@@ -15,62 +15,21 @@ import NMapsMap
15
15
import RxSwift
16
16
import SnapKit
17
17
18
- /**
19
- # MapViewController
20
- 네이버 지도를 표시하고, 제휴 업체(Partnership) 정보를 받아와 지도에 마커를 추가하는 역할을 담당하는 뷰 컨트롤러입니다.
21
-
22
- ## 기능
23
- - 숭실대학교를 기준으로 지도를 초기화합니다.
24
- - 전체 제휴 업체 목록을 네트워크로부터 가져와 마커를 배치합니다.
25
- - `UISegmentedControl`을 이용해 '내 제휴' / '전체' 등 원하는 정보만 지도 위에 표시할 수 있습니다.
26
- - 마커를 탭하면 `FloatingPanel`을 이용해 선택한 마커의 상세 정보를 표시합니다.
27
- - 지도 영역 외부를 탭하면 패널을 닫고 선택 상태를 해제합니다.
28
-
29
- ## 사용 예시
30
- 1. `MapViewController`를 생성합니다.
31
- 2. 내비게이션 컨트롤러에 `MapViewController`를 포함시켜 화면에 표시합니다.
32
- 3. 화면에 표시된 지도를 통해 제휴 업체 정보를 확인하고, 마커를 탭하면 상세 정보를 확인할 수 있습니다.
33
-
34
- - Author: **JIWOONG CHOI**
35
- - Date: 2025.01.28
36
- - SeeAlso: `MarkerDetailViewController`, `FloatingPanelController`
37
- */
38
18
final class MapViewController : BaseViewController {
39
19
// MARK: - Properties
40
20
41
- /// `PartnershipService` 인스턴스. 제휴 업체 정보를 가져오는 데 사용됩니다.
42
21
private let partnershipService = PartnershipService ( )
43
-
44
- /// `UserDepartmentService` 인스턴스. 사용자의 학부와 관련된 정보를 서버에 요청하는 객체입니다.
45
22
private let userDepartmentService = UserDepartmentService ( )
46
-
47
- /// Rx에서 사용되는 DisposeBag입니다.
48
23
private let disposeBag = DisposeBag ( )
49
-
50
- /// 네이버 지도 뷰입니다.
51
24
private var mapView : NMFMapView !
52
-
53
- /// 지도 상단에 표시되는 UISegmentedControl
54
25
private var mapSegmentedControl : UISegmentedControl !
55
-
56
- /// FloatingPanelController 인스턴스 (패널이 띄워져 있을 때 참조)
57
26
private var floatingPanelController : FloatingPanelController ?
58
-
59
- /// 현재 선택된 마커 데이터를 저장합니다.
60
27
private var selectedMarkerData : MarkerData ?
61
-
62
- /// 숭실대학교 위치 (위도, 경도). 카메라를 이동할 기본 위치로 사용됩니다.
63
28
private let soongsilUniversityLocation = NMGLatLng ( lat: 37.496389 , lng: 126.957222 )
64
-
65
- /// 지도 위에 추가된 `ESMarker` 객체들을 저장합니다.
66
29
private var esMarkers : [ ESMarker ] = [ ]
67
30
68
31
// MARK: - Life Cycle
69
32
70
- /**
71
- 화면이 메모리에 로드되었을 때 호출됩니다.
72
- 지도와 UI를 설정하고, 제휴 정보를 불러와 표시합니다.
73
- */
74
33
override func viewDidLoad( ) {
75
34
super. viewDidLoad ( )
76
35
configureNavigationBar ( )
@@ -85,12 +44,9 @@ final class MapViewController: BaseViewController {
85
44
}
86
45
87
46
// MARK: - UI Configuration
88
- private extension MapViewController {
89
- /**
90
- 네비게이션 바 스타일을 설정합니다.
91
47
92
- - Note: 배경색, 타이틀 폰트, 스크롤 시의 Appearance 등을 지정합니다.
93
- */
48
+ private extension MapViewController {
49
+ /// 네비게이션 바 스타일을 설정합니다.
94
50
func configureNavigationBar( ) {
95
51
navigationItem. title = ESTextLiteral . Map. mapNavTitle
96
52
navigationController? . isNavigationBarHidden = false
@@ -106,14 +62,7 @@ private extension MapViewController {
106
62
navigationController? . navigationBar. scrollEdgeAppearance = appearance
107
63
}
108
64
109
- // MARK: - 지도 설정
110
-
111
- /**
112
- 네이버 지도 뷰를 초기화하고 화면에 추가합니다.
113
-
114
- - Note: 지도에서 터치 이벤트를 수신하기 위해 `touchDelegate`를 `self`로 설정합니다.
115
- - SeeAlso: `moveCamera(to:zoomLevel:)`
116
- */
65
+ /// 네이버 지도 뷰를 초기화하고 화면에 추가합니다.
117
66
func configureMapView( ) {
118
67
mapView = NMFMapView ( frame: view. frame)
119
68
view. addSubview ( mapView)
@@ -123,7 +72,6 @@ private extension MapViewController {
123
72
124
73
/**
125
74
카메라를 특정 위치로 이동합니다.
126
-
127
75
- Parameters:
128
76
- location: 이동할 위치의 위도 및 경도 (`NMGLatLng`)
129
77
- zoomLevel: 줌 레벨 (`Double`)
@@ -133,13 +81,7 @@ private extension MapViewController {
133
81
mapView. moveCamera ( cameraUpdate)
134
82
}
135
83
136
- // MARK: - UISegmentedControl 설정
137
-
138
- /**
139
- 지도 상단에 `UISegmentedControl`을 추가하고 초기 설정을 적용합니다.
140
-
141
- - Note: Segmented Control 클릭 시 `segmentedControlChanged(_:)`를 통해 새로운 데이터를 불러오거나, 기존 마커를 지우고 다시 표시합니다.
142
- */
84
+ /// 지도 상단에 `UISegmentedControl`을 추가하고 초기 설정을 적용합니다.
143
85
func configureSegmentedControl( ) {
144
86
let items = [ " 전체 " , " 내 제휴 " ]
145
87
mapSegmentedControl = UISegmentedControl ( items: items)
@@ -171,23 +113,14 @@ private extension MapViewController {
171
113
view. bringSubviewToFront ( mapSegmentedControl)
172
114
}
173
115
174
- /**
175
- 세그먼트 컨트롤을 기본 상태(전체)로 초기화합니다.
176
-
177
- - Note: viewWillAppear에서 호출되어 화면이 나타날 때마다 세그먼트 컨트롤을 초기 상태로 되돌립니다.
178
- */
116
+ /// 세그먼트 컨트롤을 기본 상태(전체)로 초기화합니다.
179
117
func resetSegmentedControlToDefault( ) {
180
118
mapSegmentedControl. selectedSegmentIndex = 0
181
119
}
182
120
183
121
/**
184
122
Segmented Control 변경 이벤트 핸들러입니다.
185
-
186
123
- Parameter sender: 값을 변경한 `UISegmentedControl` 인스턴스
187
-
188
- 분기:
189
- 1. 0(기본: "내 제휴") 선택 시 전체 제휴 업체 데이터를 불러옵니다.
190
- 2. 1("전체") 선택 시 사용자의 제휴 업체 데이터(미구현)를 불러옵니다.
191
124
*/
192
125
@objc func segmentedControlChanged( _ sender: UISegmentedControl ) {
193
126
removeAllMarkersFromMap ( )
@@ -218,16 +151,15 @@ private extension MapViewController {
218
151
}
219
152
220
153
// MARK: - Marker Management
154
+
221
155
private extension MapViewController {
222
156
/**
223
157
특정 위치에 마커를 추가합니다.
224
-
225
158
- Parameters:
226
159
- location: 마커를 추가할 `NMGLatLng` 위치
227
160
- leftText: 마커 왼쪽에 표시할 텍스트
228
161
- rightText: 마커 오른쪽에 표시할 텍스트
229
162
- markerData: 해당 마커에 대응하는 `MarkerData` (상세보기용 정보)
230
- - Note: 마커를 탭하면 `presentMarkerDetailFloatingPanel(with:)`가 호출되어 상세 패널이 표시됩니다.
231
163
*/
232
164
func createAndAddMarker(
233
165
at location: NMGLatLng ,
@@ -252,11 +184,7 @@ private extension MapViewController {
252
184
esMarkers. append ( marker)
253
185
}
254
186
255
- /**
256
- 지도에 추가된 모든 마커를 제거합니다.
257
-
258
- - Note: `esMarkers` 배열에 저장된 `ESMarker`를 순회하며, 지도에서 제거(MapView 연결 해제)한 뒤 배열을 비웁니다.
259
- */
187
+ /// 지도에 추가된 모든 마커를 제거합니다.
260
188
func removeAllMarkersFromMap( ) {
261
189
for marker in esMarkers {
262
190
marker. marker. mapView = nil
@@ -266,9 +194,7 @@ private extension MapViewController {
266
194
267
195
/**
268
196
파트너십 응답 데이터를 기반으로 지도에 마커들을 생성하고 표시합니다.
269
-
270
197
- Parameter partnerships: 서버로부터 받은 파트너십 응답 데이터 배열
271
- - Note: 각 파트너십 데이터에 대해 위치 정보와 마커 데이터를 생성하여 지도에 표시합니다.
272
198
*/
273
199
func displayPartnershipsOnMap( from partnerships: [ PartnershipResponse ] ) {
274
200
for partnership in partnerships {
@@ -292,14 +218,11 @@ private extension MapViewController {
292
218
}
293
219
294
220
// MARK: - FloatingPanel Management
221
+
295
222
private extension MapViewController {
296
223
/**
297
224
마커 상세 정보를 `FloatingPanel`로 표시합니다.
298
-
299
225
- Parameter markerData: 마커 클릭 시 표시할 데이터(`MarkerData`)
300
- - Note:
301
- 1. 이미 동일한 마커가 선택되어 있고, 패널이 표시 중이면 그대로 유지합니다.
302
- 2. 새로운 마커를 탭하면, `MarkerDetailViewController`를 생성해 패널 내용으로 설정하고 부모에 추가합니다.
303
226
*/
304
227
func showMarkerDetailPanel( with markerData: MarkerData ) {
305
228
if let currentData = selectedMarkerData, currentData == markerData,
@@ -312,12 +235,7 @@ private extension MapViewController {
312
235
let detailVC = MarkerDetailViewController ( markerData: markerData)
313
236
314
237
if floatingPanelController == nil {
315
- floatingPanelController = FloatingPanelController ( )
316
- floatingPanelController? . delegate = self
317
-
318
- let appearance = SurfaceAppearance ( )
319
- appearance. cornerRadius = 15
320
- floatingPanelController? . surfaceView. appearance = appearance
238
+ initializeFloatingPanelController ( )
321
239
}
322
240
323
241
floatingPanelController? . set ( contentViewController: detailVC)
@@ -326,17 +244,33 @@ private extension MapViewController {
326
244
floatingPanelController? . addPanel ( toParent: self )
327
245
}
328
246
}
247
+
248
+ /// FloatingPanelController를 초기화하고 appearance를 설정합니다.
249
+ func initializeFloatingPanelController( ) {
250
+ floatingPanelController = FloatingPanelController ( )
251
+ floatingPanelController? . delegate = self
252
+ configureFloatingPanelAppearance ( )
253
+ }
254
+
255
+ /// FloatingPanel의 appearance를 설정합니다.
256
+ func configureFloatingPanelAppearance( ) {
257
+ let appearance = SurfaceAppearance ( )
258
+ appearance. cornerRadius = 15
259
+ floatingPanelController? . surfaceView. appearance = appearance
260
+ }
261
+
262
+ /// FloatingPanel을 제거하고 선택된 마커 데이터를 초기화합니다.
263
+ func removeFloatingPanel( ) {
264
+ floatingPanelController? . removePanelFromParent ( animated: true )
265
+ floatingPanelController = nil
266
+ selectedMarkerData = nil
267
+ }
329
268
}
330
269
331
270
// MARK: - Network Operations
332
- private extension MapViewController {
333
- /**
334
- 전체 제휴 목록을 네트워크로부터 가져오는 메서드입니다.
335
271
336
- - Note:
337
- 1. 성공 시 `baseResponse.result`에 포함된 제휴 정보를 순회하며, 지도 위에 마커를 배치합니다.
338
- 2. 실패 시 디버그 로그를 남기고, 필요 시 사용자에게 알림을 표시하도록 TODO를 남겼습니다.
339
- */
272
+ private extension MapViewController {
273
+ /// 전체 제휴 목록을 네트워크로부터 가져와 표시합니다.
340
274
func fetchAllPartnershipsAndDisplay( ) {
341
275
partnershipService. fetchAllPartnerships ( )
342
276
. subscribe (
@@ -363,14 +297,7 @@ private extension MapViewController {
363
297
. disposed ( by: disposeBag)
364
298
}
365
299
366
- /**
367
- 사용자의 학과/단과대 관련 제휴 목록을 네트워크로부터 가져와 표시하는 메서드입니다.
368
-
369
- - Note:
370
- 1. UserDepartmentService를 통해 사용자의 학과/단과대 관련 제휴 정보를 요청합니다.
371
- 2. 성공 시 받아온 제휴 정보를 지도에 마커로 표시합니다.
372
- 3. 실패 시 사용자에게 에러 알림을 표시합니다.
373
- */
300
+ /// 사용자의 학과/단과대 관련 제휴 목록을 네트워크로부터 가져와 표시합니다.
374
301
func fetchUserPartnershipsAndDisplay( ) {
375
302
userDepartmentService. getUserPartnership ( )
376
303
. subscribe (
@@ -398,10 +325,8 @@ private extension MapViewController {
398
325
}
399
326
400
327
/**
401
- 파트너십 데이터 요청 실패 시 에러 알림을 표시하는 함수입니다.
402
-
328
+ 파트너십 데이터 요청 실패 시 에러 알림을 표시합니다.
403
329
- Parameter error: 발생한 에러 객체
404
- - Note: 디버그 모드에서는 에러 상세 정보를 로그로 출력하고, 사용자에게는 간단한 에러 메시지를 알림으로 표시합니다.
405
330
*/
406
331
func showPartnershipErrorAlert( _ error: Error ) {
407
332
#if DEBUG
@@ -417,20 +342,18 @@ private extension MapViewController {
417
342
}
418
343
419
344
// MARK: - NMFMapViewTouchDelegate
345
+
420
346
extension MapViewController : NMFMapViewTouchDelegate {
421
347
/**
422
348
사용자가 지도에서 단일 탭했을 때 호출됩니다.
423
349
마커가 아닌 다른 부분을 탭하면 FloatingPanel을 제거합니다.
424
-
425
350
- Parameters:
426
351
- latlng: 탭한 위치의 `NMGLatLng`
427
352
- point: 탭한 화면 좌표(`CGPoint`)
428
353
*/
429
354
func mapView( _: NMFMapView , didTapMap latlng: NMGLatLng , point _: CGPoint ) {
430
355
if let fpc = floatingPanelController, fpc. parent != nil {
431
- fpc. removePanelFromParent ( animated: true )
432
- floatingPanelController = nil
433
- selectedMarkerData = nil
356
+ removeFloatingPanel ( )
434
357
}
435
358
#if DEBUG
436
359
print ( " 탭: \( latlng. lat) , \( latlng. lng) " )
@@ -440,16 +363,13 @@ extension MapViewController: NMFMapViewTouchDelegate {
440
363
/**
441
364
사용자가 지도에서 길게 누를 때 호출됩니다.
442
365
마커가 아닌 다른 부분을 길게 누르면 FloatingPanel을 제거합니다.
443
-
444
366
- Parameters:
445
367
- latlng: 길게 누른 위치의 `NMGLatLng`
446
368
- point: 터치된 화면 좌표(`CGPoint`)
447
369
*/
448
370
func mapView( _: NMFMapView , didLongTapMap latlng: NMGLatLng , point _: CGPoint ) {
449
371
if let fpc = floatingPanelController, fpc. parent != nil {
450
- fpc. removePanelFromParent ( animated: true )
451
- floatingPanelController = nil
452
- selectedMarkerData = nil
372
+ removeFloatingPanel ( )
453
373
}
454
374
#if DEBUG
455
375
print ( " 롱 탭: \( latlng. lat) , \( latlng. lng) " )
@@ -458,15 +378,10 @@ extension MapViewController: NMFMapViewTouchDelegate {
458
378
}
459
379
460
380
// MARK: - FloatingPanelControllerDelegate
461
- extension MapViewController : FloatingPanelControllerDelegate {
462
- /**
463
- FloatingPanel이 제거되었을 때 호출됩니다.
464
- 패널과 선택된 마커 데이터를 초기화합니다.
465
381
466
- - Parameter fpc: 제거된 `FloatingPanelController`
467
- */
382
+ extension MapViewController : FloatingPanelControllerDelegate {
383
+ /// FloatingPanel이 제거되었을 때 호출됩니다.
468
384
func floatingPanelDidRemove( _: FloatingPanelController ) {
469
- floatingPanelController = nil
470
- selectedMarkerData = nil
385
+ removeFloatingPanel ( )
471
386
}
472
387
}
0 commit comments