diff --git a/Nagaza/Sources/Application/DIContainer/SceneDIContainer/MapSceneDIContainer.swift b/Nagaza/Sources/Application/DIContainer/SceneDIContainer/MapSceneDIContainer.swift index 2df5c7d..5c77641 100644 --- a/Nagaza/Sources/Application/DIContainer/SceneDIContainer/MapSceneDIContainer.swift +++ b/Nagaza/Sources/Application/DIContainer/SceneDIContainer/MapSceneDIContainer.swift @@ -12,11 +12,26 @@ final class MapSceneDIContainer: MapFlowCoordinaterDependencies { MapViewController.create(with: makeMapViewModel(actions: actions)) } - private func makeMapViewModel(actions: MapViewModelActions) -> MapViewModel - { + private func makeMapViewModel(actions: MapViewModelActions) -> MapViewModel { MapViewModel(actions: actions) } + func makeMapSearchViewController(actions: MapSearchViewModelActions) -> MapSearchViewController { + MapSearchViewController.create(with: makeMapSearchViewModel(actions: actions)) + } + + private func makeMapUseCase() -> MapRepositoryInterface { + return MapRepository( + isStub: true, + sampleStatusCode: 200, + customEndpointClosure: nil + ) + } + + private func makeMapSearchViewModel(actions: MapSearchViewModelActions) -> MapSearchViewModel { + MapSearchViewModel(mapUseCase: makeMapUseCase(), actions: actions) + } + func makeMapFlowCoordinator(navigationController: UINavigationController) -> MapFlowCoordinator { MapFlowCoordinator( navigationController: navigationController, diff --git a/Nagaza/Sources/Data/Network/DataMapping/RecentKeywordResponseDTO.swift b/Nagaza/Sources/Data/Network/DataMapping/RecentKeywordResponseDTO.swift new file mode 100644 index 0000000..dd550a1 --- /dev/null +++ b/Nagaza/Sources/Data/Network/DataMapping/RecentKeywordResponseDTO.swift @@ -0,0 +1,40 @@ +// +// KeywordResponseDTO.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +struct RecentKeywordResponse: Decodable { + let keywordList: [RecentKeywordResponseDTO] + let page: Int + let totalPages: Int + + enum CodingKeys: String, CodingKey { + case keywordList + case page + case totalPages = "total_pages" + } +} + +struct RecentKeywordResponseDTO: Decodable { + let keyword: String +} + +extension RecentKeywordResponse { + func toDomain() -> RecentKeywordPage { + return RecentKeywordPage( + keywordList: self.keywordList.map { $0.toDomain() }, + page: self.page, + totalPages: self.totalPages + ) + } +} + +extension RecentKeywordResponseDTO { + func toDomain() -> RecentKeyword { + return RecentKeyword( + keyword: self.keyword + ) + } +} diff --git a/Nagaza/Sources/Data/Repositories/Map/MapRepository.swift b/Nagaza/Sources/Data/Repositories/Map/MapRepository.swift new file mode 100644 index 0000000..bb240e7 --- /dev/null +++ b/Nagaza/Sources/Data/Repositories/Map/MapRepository.swift @@ -0,0 +1,28 @@ +// +// MapRepository.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import RxSwift +import Moya + +final class MapRepository: ProviderProtocol { + typealias Target = MapTarget + var provider: MoyaProvider + + init(isStub: Bool, sampleStatusCode: Int, customEndpointClosure: ((Target) -> Moya.Endpoint)?) { + self.provider = Self.consProvider(isStub, sampleStatusCode, customEndpointClosure) + } +} + +extension MapRepository: MapRepositoryInterface { + func fetchRecentKeywordList() -> Single { + request( + type: RecentKeywordResponse.self, + target: .fetchRecentKeywordList + ) + .map { $0.toDomain() } + } +} diff --git a/Nagaza/Sources/Data/Repositories/Map/MapTarget+SampleData.swift b/Nagaza/Sources/Data/Repositories/Map/MapTarget+SampleData.swift new file mode 100644 index 0000000..0e8c0e4 --- /dev/null +++ b/Nagaza/Sources/Data/Repositories/Map/MapTarget+SampleData.swift @@ -0,0 +1,39 @@ +// +// MapTarget+SampleData.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import Foundation + +import Moya + +extension MapTarget { + var sampleData: Data { + switch self { + case .fetchRecentKeywordList: + return Data( + """ + { + "keywordList": [ + { + "keyword": "리그오브디저트" + }, + { + "keyword": "방탈출" + }, + { + "keyword": "풀문" + }, + { + "keyword": "마스터키" + } + ], + "page": 1, + "total_pages": 1 + } + """.utf8) + } + } +} diff --git a/Nagaza/Sources/Data/Repositories/Map/MapTarget.swift b/Nagaza/Sources/Data/Repositories/Map/MapTarget.swift new file mode 100644 index 0000000..d37c39f --- /dev/null +++ b/Nagaza/Sources/Data/Repositories/Map/MapTarget.swift @@ -0,0 +1,36 @@ +// +// MapTarget.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import Moya + +enum MapTarget { + case fetchRecentKeywordList +} + +extension MapTarget: BaseTargetType { + var path: String { + switch self { + case .fetchRecentKeywordList: + return "v1/search/keyword" + } + } + + var method: Moya.Method { + switch self { + case .fetchRecentKeywordList: + return .get + } + } + + var task: Moya.Task { + switch self { + case .fetchRecentKeywordList: + return .requestPlain + } + } +} + diff --git a/Nagaza/Sources/Domain/Entities/RecentKeyword.swift b/Nagaza/Sources/Domain/Entities/RecentKeyword.swift new file mode 100644 index 0000000..557ae0f --- /dev/null +++ b/Nagaza/Sources/Domain/Entities/RecentKeyword.swift @@ -0,0 +1,33 @@ +// +// RecentKeyword.swift +// Nagaza +// +// Created by SeungMin on 3/13/24. +// + +import Foundation + +enum SearchSection { + case home +} + +struct RecentKeyword: Hashable { + let identifier = UUID() + let keyword: String +} + +struct RecentKeywordPage { + let keywordList: [RecentKeyword] + let page: Int + let totalPages: Int +} + +extension RecentKeyword { + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } + + public static func == (lhs: RecentKeyword, rhs: RecentKeyword) -> Bool { + lhs.identifier == rhs.identifier + } +} diff --git a/Nagaza/Sources/Domain/Interfaces/Repositories/Home/HomeRepositoryInterface.swift b/Nagaza/Sources/Domain/Interfaces/Repositories/Home/HomeRepositoryInterface.swift index 4bd0205..6ed241d 100644 --- a/Nagaza/Sources/Domain/Interfaces/Repositories/Home/HomeRepositoryInterface.swift +++ b/Nagaza/Sources/Domain/Interfaces/Repositories/Home/HomeRepositoryInterface.swift @@ -5,8 +5,6 @@ // Created by SeungMin on 1/6/24. // -import Foundation - import RxSwift protocol HomeRepositoryInterface { diff --git a/Nagaza/Sources/Domain/Interfaces/Repositories/Map/MapRepositoryInterface.swift b/Nagaza/Sources/Domain/Interfaces/Repositories/Map/MapRepositoryInterface.swift new file mode 100644 index 0000000..10c2835 --- /dev/null +++ b/Nagaza/Sources/Domain/Interfaces/Repositories/Map/MapRepositoryInterface.swift @@ -0,0 +1,12 @@ +// +// MapRepositoryInterface.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import RxSwift + +protocol MapRepositoryInterface { + func fetchRecentKeywordList() -> Single +} diff --git a/Nagaza/Sources/Domain/UseCases/HomeUseCase.swift b/Nagaza/Sources/Domain/UseCases/Home/HomeUseCase.swift similarity index 100% rename from Nagaza/Sources/Domain/UseCases/HomeUseCase.swift rename to Nagaza/Sources/Domain/UseCases/Home/HomeUseCase.swift diff --git a/Nagaza/Sources/Domain/UseCases/Map/MapUseCase.swift b/Nagaza/Sources/Domain/UseCases/Map/MapUseCase.swift new file mode 100644 index 0000000..1cab6e4 --- /dev/null +++ b/Nagaza/Sources/Domain/UseCases/Map/MapUseCase.swift @@ -0,0 +1,20 @@ +// +// MapUseCase.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import RxSwift + +final class MapUseCase { + private let repository: MapRepositoryInterface + + init(roomsRepository: MapRepositoryInterface) { + self.repository = roomsRepository + } + + func fetchCafesList() -> Single { + return repository.fetchRecentKeywordList() + } +} diff --git a/Nagaza/Sources/Presentation/Base/NagazaBaseViewController.swift b/Nagaza/Sources/Presentation/Base/NagazaBaseViewController.swift index c4e8aa5..8ac8975 100644 --- a/Nagaza/Sources/Presentation/Base/NagazaBaseViewController.swift +++ b/Nagaza/Sources/Presentation/Base/NagazaBaseViewController.swift @@ -33,9 +33,13 @@ class NagazaBaseViewController: UIViewController { /// Set up Navigation Bar func navigationSetting() { - navigationController?.navigationBar.backIndicatorImage = NagazaAsset.Images.icArrowRightGray.image - navigationController?.navigationBar.backIndicatorTransitionMaskImage = NagazaAsset.Images.icArrowRightGray.image - navigationController?.navigationBar.tintColor = .white + navigationController?.navigationBar.tintColor = NagazaAsset.Colors.gray3.color + navigationItem.backBarButtonItem = UIBarButtonItem( + title: nil, + style: .plain, + target: nil, + action: nil + ) let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqueBackground() diff --git a/Nagaza/Sources/Presentation/Base/NagazaCollectionViewCell.swift b/Nagaza/Sources/Presentation/Base/NagazaCollectionViewCell.swift new file mode 100644 index 0000000..b2f9475 --- /dev/null +++ b/Nagaza/Sources/Presentation/Base/NagazaCollectionViewCell.swift @@ -0,0 +1,29 @@ +// +// NagazaCollectionViewCell.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import UIKit +import RxSwift + +class NagazaCollectionViewCell: UICollectionViewCell { + var disposeBag = DisposeBag() + + override init(frame: CGRect) { + super.init(frame: frame) + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + self.disposeBag = DisposeBag() + super.prepareForReuse() + } + + func makeUI() { } +} diff --git a/Nagaza/Sources/Presentation/Feature/Flow/MapFlowCoordinator.swift b/Nagaza/Sources/Presentation/Feature/Flow/MapFlowCoordinator.swift index d742bc2..1905d8e 100644 --- a/Nagaza/Sources/Presentation/Feature/Flow/MapFlowCoordinator.swift +++ b/Nagaza/Sources/Presentation/Feature/Flow/MapFlowCoordinator.swift @@ -9,6 +9,7 @@ import UIKit protocol MapFlowCoordinaterDependencies { func makeMapViewController(actions: MapViewModelActions) -> MapViewController + func makeMapSearchViewController(actions: MapSearchViewModelActions) -> MapSearchViewController } final class MapFlowCoordinator: Coordinator { @@ -37,13 +38,19 @@ final class MapFlowCoordinator: Coordinator { } func start() { - let actions = MapViewModelActions() + let actions = MapViewModelActions(toMapSearch: toMapSearch) let vc = dependencies.makeMapViewController(actions: actions) navigationController.setNavigationBarHidden(false, animated: false) - navigationController.pushViewController(vc, animated: false) + navigationController = UINavigationController(rootViewController: vc) mapVC = vc } + func toMapSearch() { + let actions = MapSearchViewModelActions() + let vc = dependencies.makeMapSearchViewController(actions: actions) + + navigationController.pushViewController(vc, animated: true) + } } diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/HomeViewController.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/HomeViewController.swift index b60f370..9402e20 100644 --- a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/HomeViewController.swift +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/HomeViewController.swift @@ -58,7 +58,7 @@ final class HomeViewController: NagazaBaseViewController { }() private lazy var scrollView = UIScrollView() - + private lazy var recommendedContainer: UIView = { let view = UIView() view.backgroundColor = .clear @@ -221,10 +221,10 @@ extension HomeViewController { let roomCellRegistraition = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in cell.bind(with: item) -// cell.delegate = self + // cell.delegate = self } - let headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: ElementKind.sectionHeader) { supplementaryView, elementKind, indexPath in + let headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in let sectionType = HomeSectionType(rawValue: indexPath.section) ?? .comic supplementaryView.themeLabel.text = sectionType.title } diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/Cell/ThemeCell.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/Cell/ThemeCell.swift index 97505c4..090874e 100644 --- a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/Cell/ThemeCell.swift +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/Cell/ThemeCell.swift @@ -190,7 +190,7 @@ final class ThemeCell: UICollectionViewCell { self.titleLabel.text = model.name self.branchLabel.text = model.area self.ratedLabel.text = String(model.total) - self.posterImageView.loadImage(from: model.imageUrlString) +// self.posterImageView.loadImage(from: model.imageUrlString) } private func updatePosterImage() { diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/SectionHeaderView.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/SectionHeaderView.swift index 3235a5a..f7b0d2e 100644 --- a/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/SectionHeaderView.swift +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Home/View/Subviews/SectionHeaderView.swift @@ -8,8 +8,6 @@ import UIKit final class SectionHeaderView: UICollectionReusableView { - static let identifier = String(describing: SectionHeaderView.self) - lazy var themeLabel: UILabel = { let label = UILabel() diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/Cell/SearchCollectionViewCell.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/Cell/SearchCollectionViewCell.swift new file mode 100644 index 0000000..ae0b4a4 --- /dev/null +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/Cell/SearchCollectionViewCell.swift @@ -0,0 +1,43 @@ +// +// SearchCollectionViewCell.swift +// Nagaza +// +// Created by SeungMin on 3/13/24. +// + +import UIKit + +final class SearchCollectionViewCell: NagazaCollectionViewCell { + lazy var textLabel: UILabel = { + let label = UILabel() + label.font = UIFont.ngaSubTitle1R + label.textAlignment = .center + label.textColor = NagazaAsset.Colors.black1.color + return label + }() + + lazy var removeButton: UIButton = { + let button = UIButton() + button.setImage(NagazaAsset.Images.icX.image, for: .normal) + return button + }() + + override func makeUI() { + contentView.addSubview(textLabel) + contentView.addSubview(removeButton) + + textLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(35) + $0.centerY.equalToSuperview() + } + + removeButton.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(20) + $0.centerY.equalToSuperview() + } + } + + func bind(item: RecentKeyword) { + textLabel.text = item.keyword + } +} diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapSearchViewController.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapSearchViewController.swift new file mode 100644 index 0000000..41b7221 --- /dev/null +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapSearchViewController.swift @@ -0,0 +1,148 @@ +// +// MapSearchViewController.swift +// Nagaza +// +// Created by SeungMin on 3/13/24. +// + +import UIKit + +import RxSwift + +final class MapSearchViewController: NagazaBaseViewController { + private var viewModel: MapSearchViewModel! + private var dataSource: DataSource! + + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewCompositionalLayout.searchLayout(withEstimatedHeight: 45) + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + return collectionView + }() + + static func create(with viewModel: MapSearchViewModel) -> MapSearchViewController { + let vc = MapSearchViewController() + vc.viewModel = viewModel + + return vc + } + + override func loadView() { + super.loadView() + view = collectionView + } + + override func viewDidLoad() { + super.viewDidLoad() + setDataSource() + } + + override func navigationSetting() { + navigationController?.navigationBar.isHidden = false + + let searchTextField: UISearchTextField = { + let searchTextField = UISearchTextField() + searchTextField.leftView = nil + searchTextField.placeholder = "검색어를 입력하세요" + searchTextField.backgroundColor = .white + return searchTextField + }() + + let searchButtonItem = UIBarButtonItem( + image: NagazaAsset.Images.imgSearch.image, + style: .plain, + target: nil, + action: nil + ) + + navigationItem.titleView = searchTextField + navigationItem.rightBarButtonItem = searchButtonItem + } + + override func bindViewModel() { + let initialTrigger = self.rx.viewWillAppear.map { _ in }.asDriverOnErrorJustEmpty() + + let input = MapSearchViewModel.Input(initialTrigger: initialTrigger) + + let ouput = viewModel.transform(input: input) + + ouput.recentKeywordList + .drive(with: self) { owner, recentKeywordlist in + var snapshot = Snapshot() + snapshot.appendSections([.home]) + snapshot.appendItems(recentKeywordlist, toSection: .home) + owner.dataSource.apply(snapshot) + } + .disposed(by: disposeBag) + } +} + +extension MapSearchViewController { + typealias CellType = SearchCollectionViewCell + typealias ModelType = RecentKeyword + typealias SectionType = SearchSection + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + private func setDataSource() { + let headerRegistration = UICollectionView.SupplementaryRegistration + (elementKind: UICollectionView.elementKindSectionHeader) { _,_,_ in } + + let searchCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in + cell.bind(item: item) + } + + dataSource = DataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in + return collectionView.dequeueConfiguredReusableCell(using: searchCellRegistration, for: indexPath, item: itemIdentifier) + }) + + dataSource.supplementaryViewProvider = { [weak self] (view, kind, index) in + guard let self = self else { return .none } + return self.collectionView.dequeueConfiguredReusableSupplementary( + using: headerRegistration, + for: index + ) + } + } +} + +extension UICollectionViewCompositionalLayout { + static func searchLayout(withEstimatedHeight estimatedHeight: CGFloat = 45) -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout(section: .searchSection(withEstimatedHeight: estimatedHeight)) + } +} + +extension NSCollectionLayoutSection { + static func searchSection(withEstimatedHeight estimatedHeight: CGFloat = 45) -> NSCollectionLayoutSection { + + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(estimatedHeight) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(estimatedHeight) + ) + + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item]) + + let sectionHeaderSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(54)) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: sectionHeaderSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [sectionHeader] + + return section + } +} diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapViewController.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapViewController.swift index 3ba69ea..9921010 100644 --- a/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapViewController.swift +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/MapViewController.swift @@ -15,31 +15,13 @@ import RxGesture final class MapViewController: NagazaBaseViewController { private var viewModel: MapViewModel! - lazy var mapView: MKMapView = { + private lazy var mapView: MKMapView = { let mapView = MKMapView() mapView.showsUserLocation = true return mapView }() - lazy var searchTextField: UISearchTextField = { - let searchTextField = UISearchTextField() - - let customView = UIView(frame: CGRect(x: -16, y: 0, width: 24, height: 24)) - let searchImageView = UIImageView() - searchImageView.image = NagazaAsset.Images.imgSearch.image - customView.addSubview(searchImageView) - searchImageView.frame = customView.frame - - searchTextField.leftView = nil - searchTextField.rightView = customView - searchTextField.rightViewMode = .always - searchTextField.clearButtonMode = .never - searchTextField.placeholder = "검색어를 입력하세요" - searchTextField.backgroundColor = .white - searchTextField.isOpaque = true - searchTextField.alpha = 1 - return searchTextField - }() + private lazy var mapSearchView = MapSearchView() static func create(with viewModel: MapViewModel) -> MapViewController { let vc = MapViewController() @@ -48,15 +30,19 @@ final class MapViewController: NagazaBaseViewController { return vc } - override func loadView() { - super.loadView() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) navigationController?.navigationBar.isHidden = true - view = mapView } override func makeUI() { - view.addSubview(searchTextField) - searchTextField.snp.makeConstraints { + view.addSubview(mapView) + mapView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + view.addSubview(mapSearchView) + mapSearchView.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide).inset(25) $0.leading.trailing.equalToSuperview().inset(13) $0.height.equalTo(60) @@ -70,37 +56,16 @@ final class MapViewController: NagazaBaseViewController { } override func bindViewModel() { - guard let searchImageView = self.searchTextField.rightView else { return } + let searchViewTapTrigger = mapSearchView.rx.tapGesture() + .when(.recognized) + .map { _ in }.asDriverOnErrorJustEmpty() - let searchButtonTapTrigger = searchImageView.rx.tapGesture() - .withLatestFrom(searchTextField.rx.text.map({ $0 ?? "" })) .asDriver(onErrorJustReturn: "") - - let input = MapViewModel.Input(searchButtonTapTrigger: searchButtonTapTrigger) + let input = MapViewModel.Input(searchViewTapTrigger: searchViewTapTrigger) let output = viewModel.transform(input: input) - output.coordinates - .do(onNext: { coordinates in - guard let value = coordinates else { - // TODO: 입력한 값이 없을 때 처리 - print("검색한 값이 없음!!") - return - } - }) - .map { [weak self] value in - guard let self = self else { return MKCoordinateRegion() } - guard let coordinates = value else { return self.mapView.region } - return MKCoordinateRegion( - center: - CLLocationCoordinate2D( - latitude: coordinates.latitude, - longitude: coordinates.longitude - ), - latitudinalMeters: 500, - longitudinalMeters: 500 - ) - } - .drive(mapView.rx.region) + output.mapSearch + .drive() .disposed(by: disposeBag) } } @@ -147,15 +112,15 @@ extension MapViewController: MKMapViewDelegate { } } -#if DEBUG -import SwiftUI - -struct MapViewControllerPreview: PreviewProvider { - static var previews: some View { - let actions = MapViewModelActions() - let viewModel = MapViewModel(actions: actions) - let viewController = MapViewController.create(with: viewModel) - return viewController.toPreView() - } -} -#endif +//#if DEBUG +//import SwiftUI +// +//struct MapViewControllerPreview: PreviewProvider { +// static var previews: some View { +// let actions = MapViewModelActions() +// let viewModel = MapViewModel(actions: actions) +// let viewController = MapViewController.create(with: viewModel) +// return viewController.toPreView() +// } +//} +//#endif diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/SearchHeaderView.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/SearchHeaderView.swift new file mode 100644 index 0000000..beb53f9 --- /dev/null +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/View/SearchHeaderView.swift @@ -0,0 +1,52 @@ +// +// SearchHeaderView.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import UIKit + +final class SearchHeaderView: UICollectionReusableView { + private lazy var recentTextLabel: UILabel = { + let label = UILabel() + label.text = "최근 검색" + label.font = UIFont.ngaSubTitle2B + label.textAlignment = .center + label.textColor = NagazaAsset.Colors.black.color + return label + }() + + private lazy var removaAllButton: UIButton = { + let button = UIButton() + button.setAttributedTitle(NSAttributedString(string: "모두 지우기", attributes: [ + NSAttributedString.Key.font: UIFont.ngaSubTitle2B + + ]), for: .normal) + button.setTitleColor(NagazaAsset.Colors.gray3.color, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func makeUI() { + addSubview(recentTextLabel) + recentTextLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(24) + $0.centerY.equalToSuperview() + } + + addSubview(removaAllButton) + removaAllButton.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(21) + $0.centerY.equalToSuperview() + } + } +} diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapSearchViewModel.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapSearchViewModel.swift new file mode 100644 index 0000000..edafc4f --- /dev/null +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapSearchViewModel.swift @@ -0,0 +1,44 @@ +// +// MapSearchViewModel.swift +// Nagaza +// +// Created by SeungMin on 3/13/24. +// + +import RxCocoa + +struct MapSearchViewModelActions { + +} + +final class MapSearchViewModel: ViewModelType { + private let mapUseCase: MapRepositoryInterface + private let actions: MapSearchViewModelActions! + + struct Input { + let initialTrigger: Driver + } + + struct Output { + let recentKeywordList: Driver<[RecentKeyword]> + } + + init( + mapUseCase: MapRepositoryInterface, + actions: MapSearchViewModelActions + ) { + self.mapUseCase = mapUseCase + self.actions = actions + } + + func transform(input: Input) -> Output { + let recentKeywordList = input.initialTrigger + .flatMapLatest { [weak self] _ in + guard let self = self else { return Driver<[RecentKeyword]>.just([]) } + return self.mapUseCase.fetchRecentKeywordList().map { $0.keywordList } + .asDriver(onErrorJustReturn: []) + } + + return Output(recentKeywordList: recentKeywordList) + } +} diff --git a/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapViewModel.swift b/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapViewModel.swift index 93555e9..77602d3 100644 --- a/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapViewModel.swift +++ b/Nagaza/Sources/Presentation/Feature/TabBar/Map/ViewModel/MapViewModel.swift @@ -5,24 +5,23 @@ // Created by 전성훈 on 2023/10/20. // -import MapKit - import RxSwift import RxCocoa struct MapViewModelActions { - + var toMapSearch: () -> Void } final class MapViewModel: ViewModelType { +// private let mapUseCase: MapRepositoryInterface private let actions: MapViewModelActions! struct Input { - let searchButtonTapTrigger: Driver + let searchViewTapTrigger: Driver } struct Output { - let coordinates: Driver + let mapSearch: Driver } init(actions: MapViewModelActions) { @@ -30,42 +29,19 @@ final class MapViewModel: ViewModelType { } func transform(input: Input) -> Output { - let coordinatesRelay = PublishRelay() - - let searchRequestSequence = input.searchButtonTapTrigger - .do(onNext: { searchText in - guard !searchText.isEmpty else { return } - - let searchRequest = MKLocalSearch.Request() - searchRequest.naturalLanguageQuery = searchText - - let localSearch = MKLocalSearch(request: searchRequest) - localSearch.start { (response, error) in - guard let response = response else { - if let error = error { - print("검색 중 에러 발생: \(error.localizedDescription)") - coordinatesRelay.accept(nil) - } - return - } - - guard let first = response.mapItems.first else { return } - - coordinatesRelay.accept( - CLLocationCoordinate2D( - latitude: first.placemark.coordinate.latitude, - longitude: first.placemark.coordinate.longitude - ) - ) - } - }) - - let coordinates = coordinatesRelay.asDriver(onErrorJustReturn: nil) - .withLatestFrom(searchRequestSequence) { coordinates, _ in - print("찾을 좌표 - \(coordinates)") - return coordinates + let mapSearch = input.searchViewTapTrigger + .do { [weak self] _ in + guard let self = self else { return } + self.toMapSearch() } + .asDriver() - return Output(coordinates: coordinates) + return Output(mapSearch: mapSearch) + } +} + +extension MapViewModel { + func toMapSearch() { + actions.toMapSearch() } } diff --git a/Nagaza/Sources/Presentation/Utils/Components/MapSearchView.swift b/Nagaza/Sources/Presentation/Utils/Components/MapSearchView.swift new file mode 100644 index 0000000..abe30d5 --- /dev/null +++ b/Nagaza/Sources/Presentation/Utils/Components/MapSearchView.swift @@ -0,0 +1,44 @@ +// +// MapSearchView.swift +// Nagaza +// +// Created by SeungMin on 3/14/24. +// + +import UIKit + +final class MapSearchView: NagazaBaseView { + lazy var textLabel: UILabel = { + let label = UILabel() + label.text = "검색어를 입력하세요" + label.font = UIFont.ngaSubTitle2R + label.textAlignment = .center + label.textColor = NagazaAsset.Colors.gray4.color + return label + }() + + lazy var searchImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = NagazaAsset.Images.imgSearch.image + return imageView + }() + + override func makeUI() { + self.backgroundColor = NagazaAsset.Colors.white.color + self.layer.cornerRadius = 15 + self.clipsToBounds = true + + self.addSubview(textLabel) + textLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(22) + $0.centerY.equalToSuperview() + } + + self.addSubview(searchImageView) + searchImageView.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(23) + $0.centerY.equalToSuperview() + $0.width.height.equalTo(24) + } + } +} diff --git a/Nagaza/Sources/Presentation/Utils/Extensions/UICollectionViewCompositionalLayout+.swift b/Nagaza/Sources/Presentation/Utils/Extensions/UICollectionViewCompositionalLayout+.swift index 2fba15b..e305ecb 100644 --- a/Nagaza/Sources/Presentation/Utils/Extensions/UICollectionViewCompositionalLayout+.swift +++ b/Nagaza/Sources/Presentation/Utils/Extensions/UICollectionViewCompositionalLayout+.swift @@ -7,15 +7,6 @@ import UIKit -struct ElementKind { - static let badge = "badge-element-kind" - static let background = "background-element-kind" - static let sectionHeader = "section-header-element-kind" - static let sectionFooter = "section-footer-element-kind" - static let layoutHeader = "layout-header-element-kind" - static let layoutFooter = "layout-footer-element-kind" -} - extension UICollectionViewCompositionalLayout { static func listLayout(withEstimatedHeight estimatedHeight: CGFloat = 100) -> UICollectionViewCompositionalLayout { @@ -58,7 +49,7 @@ extension NSCollectionLayoutSection { let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: sectionHeaderSize, - elementKind: ElementKind.sectionHeader, + elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) @@ -104,7 +95,7 @@ extension NSCollectionLayoutSection { let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: sectionHeaderSize, - elementKind: ElementKind.sectionHeader, + elementKind: UICollectionView.elementKindSectionHeader, alignment: .top )