From ada691686be361d67f2f51540a6ad5556ee59379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=80=E1=85=B5=E1=84=92?= =?UTF-8?q?=E1=85=A7=E1=86=AB?= Date: Tue, 13 May 2025 02:12:21 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor/#147:=20Deletate=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EB=B0=8F=20Extension=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scene/Map/MapView/MapViewController.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift index 88ce0efa..f4d7417b 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift @@ -12,7 +12,7 @@ import RxGesture import RxSwift import SnapKit -class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NMFMapViewTouchDelegate, NMFMapViewCameraDelegate, UIGestureRecognizerDelegate { +class MapViewController: BaseViewController, View { typealias Reactor = MapReactor fileprivate struct CoordinateKey: Hashable { @@ -1485,8 +1485,8 @@ class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NM } } - // MARK: - CLLocationManagerDelegate - extension MapViewController { +// MARK: - CLLocationManagerDelegate +extension MapViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } @@ -1506,8 +1506,8 @@ class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NM } } - // MARK: - NMFMapViewTouchDelegate - extension MapViewController { +// MARK: - NMFMapViewTouchDelegate +extension MapViewController: NMFMapViewTouchDelegate { func mapView(_ mapView: NMFMapView, didTap marker: NMFMarker) -> Bool { if let clusterData = marker.userInfo["clusterData"] as? ClusterMarkerData { return handleRegionalClusterTap(marker, clusterData: clusterData) @@ -1543,8 +1543,8 @@ class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NM } } - // MARK: - NMFMapViewCameraDelegate - extension MapViewController { +// MARK: - NMFMapViewCameraDelegate +extension MapViewController: NMFMapViewCameraDelegate { func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) { if reason == NMFMapChangedByGesture && !isMovingToMarker { resetSelectedMarker() @@ -1576,8 +1576,8 @@ class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NM cameraIdle.onNext(()) } } - // MARK: - UIGestureRecognizerDelegate - extension MapViewController { +// MARK: - UIGestureRecognizerDelegate +extension MapViewController: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } From 784e51eb53856dc56249b70845e90d7d289471e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=80=E1=85=B5=E1=84=92?= =?UTF-8?q?=E1=85=A7=E1=86=AB?= Date: Tue, 13 May 2025 17:30:14 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor/#147:=20=EB=A7=A4=EC=A7=81?= =?UTF-8?q?=EB=84=98=EB=B2=84=20enum=20constaranits=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scene/Map/MapView/MapViewController.swift | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift index f4d7417b..a28f63e1 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift @@ -20,11 +20,23 @@ class MapViewController: BaseViewController, View { let lng: Int init(latitude: Double, longitude: Double) { - self.lat = Int(latitude * 1_000_00) - self.lng = Int(longitude * 1_000_00) + self.lat = Int(latitude * Constants.coordinateMultiplier) + self.lng = Int(longitude * Constants.coordinateMultiplier) } } + private enum Constants { + static let carouselHeight: CGFloat = 140 + static let carouselBottomOffset: CGFloat = -24 + static let tooltipMarkerHeight: CGFloat = 32 + static let tooltipYOffset: CGFloat = 14 + static let coordinateMultiplier: Double = 100_000 + static let cameraDebounceMs: Int = 300 + static let swipeDuration: TimeInterval = 0.3 + static let panVelocityThreshold: CGFloat = 500 + static let middleRatio: CGFloat = 0.3 + static let defaultZoom: Double = 15.0 + } var currentTooltipView: UIView? var currentTooltipStores: [MapPopUpStore] = [] var currentTooltipCoordinate: NMGLatLng? @@ -180,7 +192,7 @@ class MapViewController: BaseViewController, View { private func setupMapViewRxObservables() { mainView.mapView.addCameraDelegate(delegate: self) cameraIdle - .debounce(.milliseconds(300), scheduler: MainScheduler.instance) + .debounce(.milliseconds(Constants.cameraDebounceMs), scheduler: MainScheduler.instance) .map { [unowned self] in let bounds = self.getVisibleBounds() return MapReactor.Action.viewportChanged( @@ -230,11 +242,10 @@ class MapViewController: BaseViewController, View { } let markerPoint = self.mainView.mapView.projection.point(from: marker.position) - let markerHeight: CGFloat = 32 - + let markerHeight = Constants.tooltipMarkerHeight tooltipView.frame = CGRect( x: markerPoint.x, - y: markerPoint.y - markerHeight - tooltipView.frame.height - 14, + y: markerPoint.y - markerHeight - tooltipView.frame.height - Constants.tooltipYOffset, width: tooltipView.frame.width, height: tooltipView.frame.height ) @@ -255,8 +266,8 @@ class MapViewController: BaseViewController, View { view.addSubview(carouselView) carouselView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() - make.height.equalTo(140) - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24) + make.height.equalTo(Constants.carouselHeight) + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(Constants.carouselBottomOffset) } carouselView.isHidden = true mainView.mapView.touchDelegate = self @@ -282,7 +293,7 @@ class MapViewController: BaseViewController, View { mapViewTapGesture.delegate = self } - private let defaultZoomLevel: Double = 15.0 + private let defaultZoomLevel: Double = Constants.defaultZoom private func setupPanAndSwipeGestures() { storeListViewController.mainView.grabberHandle.rx.swipeGesture(.up) .skip(1) @@ -344,7 +355,7 @@ class MapViewController: BaseViewController, View { let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( lat: location.coordinate.latitude, lng: location.coordinate.longitude - ), zoomTo: 15.0) + ), zoomTo: Constants.defaultZoom) self.mainView.mapView.moveCamera(cameraUpdate) } @@ -445,7 +456,7 @@ class MapViewController: BaseViewController, View { let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( lat: store.latitude, lng: store.longitude - ), zoomTo: 15.0) + ), zoomTo: Constants.defaultZoom) cameraUpdate.animation = .easeIn cameraUpdate.animationDuration = 0.3 self.mainView.mapView.moveCamera(cameraUpdate) @@ -503,7 +514,7 @@ class MapViewController: BaseViewController, View { let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( lat: firstStore.latitude, lng: firstStore.longitude - ), zoomTo: 15.0) + ), zoomTo: Constants.defaultZoom) cameraUpdate.animation = .easeIn cameraUpdate.animationDuration = 0.3 self.mainView.mapView.moveCamera(cameraUpdate) @@ -514,7 +525,7 @@ class MapViewController: BaseViewController, View { // MARK: - List View Control private func toggleListView() { - UIView.animate(withDuration: 0.3) { + UIView.animate(withDuration: Constants.swipeDuration) { let middleOffset = -self.view.frame.height * 0.7 self.listViewTopConstraint?.update(offset: middleOffset) self.modalState = .middle @@ -611,9 +622,9 @@ class MapViewController: BaseViewController, View { let middleY = view.frame.height * 0.3 let targetState: ModalState - if velocity.y > 500 { + if velocity.y > Constants.panVelocityThreshold { targetState = .bottom - } else if velocity.y < -500 { + } else if velocity.y < -Constants.panVelocityThreshold { targetState = .top } else if currentOffset < middleY * 0.7 { targetState = .top @@ -1496,7 +1507,7 @@ extension MapViewController: CLLocationManagerDelegate { currentCarouselStores = [] let position = NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude) - let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: 15.0) + let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: Constants.defaultZoom) mainView.mapView.moveCamera(cameraUpdate) { [weak self] _ in guard let self = self else { return } self.findAndShowNearestStore(from: location) From 293075e1a080a8cf0f155e4038ed6a7babe3761c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=80=E1=85=B5=E1=84=92?= =?UTF-8?q?=E1=85=A7=E1=86=AB?= Date: Wed, 18 Jun 2025 11:27:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor/#156:=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=EB=B7=B0=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=EA=B0=80=EB=8A=A5=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=EB=B7=B0=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminBottomSheetReactor.swift | 6 - .../AdminBottomSheetView.swift | 69 ++++---- .../AdminBottomSheetViewController.swift | 161 ++++++++++-------- .../Presentation/Scene/Admin/AdminView.swift | 2 + .../Scene/Admin/AdminViewController.swift | 29 ++++ .../Scene/Map/MapView/MapViewController.swift | 146 ++++------------ 6 files changed, 198 insertions(+), 215 deletions(-) diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift index 6fd8b3b4..5dcdbebb 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift @@ -1,9 +1,3 @@ -// -// AdminBottomSheetReactor.swift -// Poppool -// -// Created by 김기현 on 1/13/25. -// import Foundation import ReactorKit diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift index 09248345..4f662af6 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift @@ -1,8 +1,6 @@ import UIKit - import DesignSystem import Infrastructure - import ReactorKit import RxCocoa import RxSwift @@ -39,7 +37,6 @@ final class AdminBottomSheetView: UIView { let closeButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_xmark"), for: .normal) - button.tintColor = .black return button }() @@ -78,16 +75,12 @@ final class AdminBottomSheetView: UIView { trailing: 20 ) section.interGroupSpacing = 16 - return section } - - let collectionView = UICollectionView( - frame: .zero, - collectionViewLayout: layout - ) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .clear collectionView.isScrollEnabled = false + // Register cell here if needed, e.g. collectionView.register(TagSectionCell.self, forCellWithReuseIdentifier: TagSectionCell.identifiers) return collectionView }() @@ -102,10 +95,7 @@ final class AdminBottomSheetView: UIView { ) button.isEnabled = false button.contentEdgeInsets = UIEdgeInsets( - top: 9, - left: 16, - bottom: 9, - right: 12 + top: 9, left: 16, bottom: 9, right: 12 ) return button }() @@ -120,10 +110,7 @@ final class AdminBottomSheetView: UIView { ) button.isEnabled = false button.contentEdgeInsets = UIEdgeInsets( - top: 9, - left: 16, - bottom: 9, - right: 12 + top: 9, left: 16, bottom: 9, right: 12 ) return button }() @@ -168,7 +155,7 @@ final class AdminBottomSheetView: UIView { private func setupConstraints() { containerView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - make.top.equalTo(headerView.snp.top) + make.top.equalToSuperview() } headerView.snp.makeConstraints { make in @@ -212,18 +199,42 @@ final class AdminBottomSheetView: UIView { } // MARK: - Public Methods - func updateContentVisibility(isCategorySelected: Bool) { - Logger.log("높이 변경 시작: \(isCategorySelected ? "카테고리" : "상태값")", category: .debug) - - let newHeight: CGFloat = isCategorySelected ? 200 : 160 - - // 애니메이션 없이 바로 적용 - contentHeightConstraint?.update(offset: newHeight) - contentCollectionView.invalidateIntrinsicContentSize() + func calculateCollectionViewHeight(for items: [String]) -> CGFloat { + let sectionInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + let itemSpacing: CGFloat = 12 + let lineSpacing: CGFloat = 16 + + let collectionViewWidth = UIScreen.main.bounds.width + let availableWidth = collectionViewWidth - sectionInsets.left - sectionInsets.right + + var currentRowWidth: CGFloat = 0 + var numberOfRows = 1 + + for (index, item) in items.enumerated() { + let text = item as NSString + let textSize = text.size(withAttributes: [ + .font: UIFont.korFont(style: .medium, size: 13) + ]) + let itemWidth = textSize.width + 32 // padding: 16 left/right each + + if index == 0 { + currentRowWidth = itemWidth + } else { + let widthWithSpacing = currentRowWidth + itemSpacing + itemWidth + if widthWithSpacing > availableWidth { + numberOfRows += 1 + currentRowWidth = itemWidth + } else { + currentRowWidth = widthWithSpacing + } + } + } - setNeedsLayout() - layoutIfNeeded() + let itemHeight: CGFloat = 36 + let totalHeight = sectionInsets.top + sectionInsets.bottom + + (CGFloat(numberOfRows) * itemHeight) + + (CGFloat(max(0, numberOfRows - 1)) * lineSpacing) - Logger.log("높이 변경 완료", category: .debug) + return totalHeight } } diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift index a02c00e7..95ddb295 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift @@ -1,8 +1,6 @@ import UIKit - import DesignSystem import Infrastructure - import ReactorKit import RxCocoa import RxSwift @@ -14,9 +12,15 @@ final class AdminBottomSheetViewController: BaseViewController, View { // MARK: - Properties private let mainView = AdminBottomSheetView() - private let dimmedView = UIView() + private lazy var dimmedView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.4) + view.alpha = 0 + return view + }() var disposeBag = DisposeBag() private var containerViewBottomConstraint: Constraint? + private var containerHeightConstraint: Constraint? private var tagSection: TagSection? var onSave: (([String]) -> Void)? @@ -43,37 +47,30 @@ final class AdminBottomSheetViewController: BaseViewController, View { private func setupViews() { view.backgroundColor = .clear - Logger.log("초기 뷰 계층:", category: .debug) + view.addSubview(dimmedView) + dimmedView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } view.addSubview(mainView) - mainView.isUserInteractionEnabled = true - mainView.containerView.isUserInteractionEnabled = true - mainView.closeButton.isUserInteractionEnabled = true - mainView.segmentedControl.isUserInteractionEnabled = true - mainView.headerView.isUserInteractionEnabled = true - mainView.snp.makeConstraints { make in make.left.right.equalToSuperview() - make.height.equalTo(view.bounds.height * 0.45) + containerHeightConstraint = make.height.greaterThanOrEqualTo(400).constraint containerViewBottomConstraint = make.bottom.equalTo(view.snp.bottom).constraint } - Logger.log("mainView 추가 후 계층:", category: .debug) - - dimmedView.backgroundColor = .black.withAlphaComponent(0.4) - dimmedView.alpha = 0 - dimmedView.isUserInteractionEnabled = false + setupGestures() + } - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmedViewTapped)) + private func setupGestures() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) + tapGesture.delegate = self dimmedView.addGestureRecognizer(tapGesture) - tapGesture.cancelsTouchesInView = true // 터치 이벤트가 다른 뷰로 전달되도록 설정 - view.insertSubview(dimmedView, belowSubview: mainView) - - dimmedView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } + dimmedView.isUserInteractionEnabled = true + } - Logger.log("최종 뷰 계층:", category: .debug) + @objc private func handleTapDimmedView() { + hideBottomSheet() } private func setupCollectionView() { @@ -86,11 +83,9 @@ final class AdminBottomSheetViewController: BaseViewController, View { // MARK: - Binding func bind(reactor: Reactor) { mainView.segmentedControl.rx.selectedSegmentIndex - .do(onNext: { _ in - }) - .map { Reactor.Action.segmentChanged($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) + .map { Reactor.Action.segmentChanged($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) mainView.resetButton.rx.tap .map { Reactor.Action.resetFilters } @@ -98,46 +93,48 @@ final class AdminBottomSheetViewController: BaseViewController, View { .disposed(by: disposeBag) mainView.contentCollectionView.rx.itemSelected - .withLatestFrom(reactor.state) { indexPath, state -> Reactor.Action in - let title = state.activeSegment == 0 ? - state.statusOptions[indexPath.item] : - state.categoryOptions[indexPath.item] - - return state.activeSegment == 0 ? - .toggleStatusOption(title) : - .toggleCategoryOption(title) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state.map { state in - let items = state.activeSegment == 0 ? - state.statusOptions : - state.categoryOptions - let selectedItems = state.activeSegment == 0 ? - state.selectedStatusOptions : - state.selectedCategoryOptions - - return items.map { - TagSectionCell.Input( - title: $0, - isSelected: selectedItems.contains($0), - id: nil - ) + .withLatestFrom(reactor.state) { indexPath, state -> Reactor.Action in + let title = state.activeSegment == 0 ? + state.statusOptions[indexPath.item] : + state.categoryOptions[indexPath.item] + + return state.activeSegment == 0 ? + .toggleStatusOption(title) : + .toggleCategoryOption(title) } - } - .bind(to: mainView.contentCollectionView.rx.items( - cellIdentifier: TagSectionCell.identifiers, - cellType: TagSectionCell.self - )) { _, item, cell in - cell.injection(with: item) - } - .disposed(by: disposeBag) + .bind(to: reactor.action) + .disposed(by: disposeBag) + reactor.state + .map { state in + let items = state.activeSegment == 0 ? + state.statusOptions : + state.categoryOptions + let selectedItems = state.activeSegment == 0 ? + state.selectedStatusOptions : + state.selectedCategoryOptions + + return items.map { + TagSectionCell.Input( + title: $0, + isSelected: selectedItems.contains($0), + id: nil + ) + } + } + .bind(to: mainView.contentCollectionView.rx.items( + cellIdentifier: TagSectionCell.identifiers, + cellType: TagSectionCell.self + )) { _, item, cell in + cell.injection(with: item) + } + .disposed(by: disposeBag) + + // 세그먼트 변경 시 전체 시트 높이 업데이트 reactor.state.map { $0.activeSegment } .distinctUntilChanged() - .bind { [weak self] index in - self?.mainView.updateContentVisibility(isCategorySelected: index == 1) + .bind { [weak self] _ in + self?.updateContainerHeight() } .disposed(by: disposeBag) @@ -167,7 +164,6 @@ final class AdminBottomSheetViewController: BaseViewController, View { } .disposed(by: disposeBag) - // View Events mainView.closeButton.rx.tap .bind { [weak self] in self?.hideBottomSheet() @@ -189,13 +185,30 @@ final class AdminBottomSheetViewController: BaseViewController, View { .disposed(by: disposeBag) } - // MARK: - Actions - @objc private func dimmedViewTapped() { - hideBottomSheet() + // MARK: - Height Management + private func updateContainerHeight() { + guard let reactor = reactor else { return } + + let items = reactor.currentState.activeSegment == 0 ? + reactor.currentState.statusOptions : + reactor.currentState.categoryOptions + + let collectionViewHeight = mainView.calculateCollectionViewHeight(for: items) + + let totalHeight = 60 + 50 + collectionViewHeight + 80 + 52 + 100 + + let finalHeight = min(max(totalHeight, 400), UIScreen.main.bounds.height * 0.8) + + containerHeightConstraint?.update(offset: finalHeight) + + self.view.layoutIfNeeded() } // MARK: - Show/Hide func showBottomSheet() { + // 초기 높이 설정 + updateContainerHeight() + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { self.dimmedView.alpha = 1 self.containerViewBottomConstraint?.update(offset: 0) @@ -218,3 +231,13 @@ final class AdminBottomSheetViewController: BaseViewController, View { Logger.log("BottomSheet deinit", category: .debug) } } + +extension AdminBottomSheetViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if gestureRecognizer.view == dimmedView { + let touchPoint = touch.location(in: view) + return !mainView.containerView.frame.contains(touchPoint) + } + return true + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift index 7393cd2a..36dc0b91 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift @@ -97,6 +97,8 @@ final class AdminView: UIView { override init(frame: CGRect) { super.init(frame: frame) setupLayout() + tableView.backgroundView = UIView() + tableView.backgroundView?.isUserInteractionEnabled = true } required init?(coder: NSCoder) { diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift index c599adae..f4750e74 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift @@ -46,6 +46,11 @@ final class AdminViewController: BaseViewController, View { mainView.logoImageView.isUserInteractionEnabled = true mainView.logoImageView.addGestureRecognizer(logoTapGesture) mainView.tableView.register(AdminStoreCell.self, forCellReuseIdentifier: AdminStoreCell.identifier) + if let tableBgView = mainView.tableView.backgroundView { + let tableBackgroundTap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tableBgView.addGestureRecognizer(tableBackgroundTap) + } + mainView.tableView.delegate = self } override func viewWillAppear(_ animated: Bool) { @@ -294,3 +299,27 @@ final class AdminViewController: BaseViewController, View { .disposed(by: disposeBag) } } + +@objc private extension AdminViewController { + func dismissKeyboard() { + view.endEditing(true) + } +} + +// MARK: - UITableViewDelegate +extension AdminViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + view.endEditing(true) + guard let store = reactor?.currentState.storeList[indexPath.row] else { return } + let detailReactor = DetailReactor( + popUpID: store.id, + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) + let detailVC = DetailController() + detailVC.reactor = detailReactor + navigationController?.pushViewController(detailVC, animated: true) + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift index 4032dbdf..1b07aabf 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift @@ -15,6 +15,8 @@ import RxSwift import SnapKit class MapViewController: BaseViewController, View { + // 최초 뷰포트 진입 여부 플래그 + private var isFirstViewportEntry = true typealias Reactor = MapReactor fileprivate struct CoordinateKey: Hashable { @@ -1229,101 +1231,38 @@ class MapViewController: BaseViewController, View { .bind(to: reactor.action) .disposed(by: disposeBag) - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .filter { !$0.isEmpty } - .take(1) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in - guard let self = self else { return } - - if let location = self.locationManager.location { - self.findAndShowNearestStore(from: location) - } else if let firstStore = stores.first, - let marker = self.findMarkerForStore(for: firstStore) { - _ = self.handleSingleStoreTap(marker, store: firstStore) - } - - self.currentStores = stores - self.updateMapWithClustering() - }) - .disposed(by: disposeBag) - + // 최초 진입시에만 자동 포커싱/캐러셀 동작 reactor.state .map { $0.viewportStores } .distinctUntilChanged() - .throttle(.milliseconds(200), scheduler: MainScheduler.instance) + .filter { !$0.isEmpty } .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] stores in guard let self = self else { return } - - let effectiveViewport = self.getEffectiveViewport() - let bounds = self.getVisibleBounds() - - let visibleStores = stores.filter { store in - let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude) - return NMGLatLngBounds(southWest: bounds.southWest, northEast: bounds.northEast).contains(storePosition) - } - self.currentStores = visibleStores - - let currentZoom = self.mainView.mapView.zoomLevel - let level = MapZoomLevel.getLevel(from: Float(currentZoom)) - - if level == .detailed && !visibleStores.isEmpty { - let effectiveStores = visibleStores.filter { store in - let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude) - return effectiveViewport.contains(storePosition) - } - - self.currentCarouselStores = visibleStores - self.carouselView.updateCards(visibleStores) - self.carouselView.isHidden = false - self.mainView.setStoreCardHidden(false, animated: true) - - if let currentMarker = self.currentMarker { - if let currentStore = currentMarker.userInfo["storeData"] as? MapPopUpStore, - let index = visibleStores.firstIndex(where: { $0.id == currentStore.id }) { - self.carouselView.scrollToCard(index: index) - } else if let storeArray = currentMarker.userInfo["storeData"] as? [MapPopUpStore], - let firstStore = storeArray.first, - let index = visibleStores.firstIndex(where: { $0.id == firstStore.id }) { - self.carouselView.scrollToCard(index: index) - } else { - // 선택된 마커가 현재 뷰포트에 없는 경우 - self.updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false) - self.currentMarker = nil - - // 첫 번째 스토어의 마커를 선택 상태로 설정 - if let firstStore = visibleStores.first, - let marker = self.findMarkerForStore(for: firstStore) { - self.updateMarkerStyle(marker: marker, selected: true, isCluster: false) - self.currentMarker = marker - } - - self.carouselView.scrollToCard(index: 0) - } - } else { - if let firstStore = visibleStores.first, - let marker = self.findMarkerForStore(for: firstStore) { - self.updateMarkerStyle(marker: marker, selected: true, isCluster: false) - self.currentMarker = marker - } - self.carouselView.scrollToCard(index: 0) - } - } else { - // 클러스터 레벨이거나 마커가 없는 경우 - self.carouselView.isHidden = true - self.carouselView.updateCards([]) - self.currentCarouselStores = [] - self.mainView.setStoreCardHidden(true, animated: true) - - if level == .detailed && visibleStores.isEmpty { - // 개별 마커 레벨인데 마커가 없는 경우 토스트 표시 - self.showNoMarkersToast() + // 최초 진입시에만 자동 포커싱/캐러셀 동작 + if self.isFirstViewportEntry { + self.isFirstViewportEntry = false + + if let location = self.locationManager.location { + self.findAndShowNearestStore(from: location) + } else if let firstStore = stores.first, + let marker = self.findMarkerForStore(for: firstStore) { + _ = self.handleSingleStoreTap(marker, store: firstStore) } } + self.currentStores = stores + self.updateMapWithClustering() + }) + .disposed(by: disposeBag) + // 지도 이동시 자동 캐러셀/포커싱 등 UI 업데이트 제거 + reactor.state + .map { $0.viewportStores } + .distinctUntilChanged() + .throttle(.milliseconds(200), scheduler: MainScheduler.instance) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] stores in + guard let self = self else { return } self.updateMapWithClustering() }) .disposed(by: disposeBag) @@ -1443,26 +1382,10 @@ class MapViewController: BaseViewController, View { } func handleMicroClusterTap(_ marker: NMFMarker, storeArray: [MapPopUpStore]) -> Bool { - if currentMarker == marker { - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - currentTooltipStores = [] - currentTooltipCoordinate = nil - - carouselView.isHidden = true - carouselView.updateCards([]) - currentCarouselStores = [] - updateMarkerStyle(marker: marker, selected: false, isCluster: false, count: storeArray.count) - - currentMarker = nil - isMovingToMarker = false - return false - } - - isMovingToMarker = true - currentTooltipView?.removeFromSuperview() currentTooltipView = nil + currentTooltipStores = [] + currentTooltipCoordinate = nil if let previousMarker = currentMarker { updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false) @@ -1471,22 +1394,23 @@ class MapViewController: BaseViewController, View { updateMarkerStyle(marker: marker, selected: true, isCluster: false, count: storeArray.count) currentMarker = marker + // 3. 캐러셀/툴팁 갱신 currentCarouselStores = storeArray carouselView.updateCards(storeArray) carouselView.isHidden = false carouselView.scrollToCard(index: 0) - mainView.setStoreCardHidden(false, animated: true) + let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position) cameraUpdate.animation = .easeIn cameraUpdate.animationDuration = 0.3 mainView.mapView.moveCamera(cameraUpdate) - if storeArray.count > 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in - guard let self = self else { return } - self.configureTooltip(for: marker, stores: storeArray) - self.isMovingToMarker = false - } + + // 4. 툴팁 갱신 및 위치 재계산 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + guard let self = self else { return } + self.configureTooltip(for: marker, stores: storeArray) + self.isMovingToMarker = false } return true From 1b3b1018ca2f7061a4f475b7ddb5ad71d5d43acb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Jul 2025 06:45:17 +0000 Subject: [PATCH 4/4] style/#156: Apply SwiftLint autocorrect --- .../Admin/AdminBottomSheet/AdminBottomSheetReactor.swift | 1 - .../Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift | 6 ++---- .../AdminBottomSheet/AdminBottomSheetViewController.swift | 2 +- .../Presentation/Scene/Map/MapView/MapViewController.swift | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift index 5dcdbebb..dcc225a2 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift @@ -1,4 +1,3 @@ - import Foundation import ReactorKit diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift index 4f662af6..ecdd6c73 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift @@ -1,10 +1,10 @@ -import UIKit import DesignSystem import Infrastructure import ReactorKit import RxCocoa import RxSwift import SnapKit +import UIKit final class AdminBottomSheetView: UIView { @@ -231,10 +231,8 @@ final class AdminBottomSheetView: UIView { } let itemHeight: CGFloat = 36 - let totalHeight = sectionInsets.top + sectionInsets.bottom + + return sectionInsets.top + sectionInsets.bottom + (CGFloat(numberOfRows) * itemHeight) + (CGFloat(max(0, numberOfRows - 1)) * lineSpacing) - - return totalHeight } } diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift index 95ddb295..b27bb85b 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift @@ -1,10 +1,10 @@ -import UIKit import DesignSystem import Infrastructure import ReactorKit import RxCocoa import RxSwift import SnapKit +import UIKit final class AdminBottomSheetViewController: BaseViewController, View { diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift index 1b07aabf..86f9d4e2 100644 --- a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift @@ -627,7 +627,7 @@ class MapViewController: BaseViewController, View { if velocity.y > Constants.panVelocityThreshold { targetState = .bottom - } else if velocity.y < -Constants.panVelocityThreshold { + } else if velocity.y < -Constants.panVelocityThreshold { targetState = .top } else if currentOffset < middleY * 0.7 { targetState = .top @@ -1261,7 +1261,7 @@ class MapViewController: BaseViewController, View { .distinctUntilChanged() .throttle(.milliseconds(200), scheduler: MainScheduler.instance) .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in + .subscribe(onNext: { [weak self] _ in guard let self = self else { return } self.updateMapWithClustering() })