์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ—บ๏ธ iOS ์œ„์น˜ ์„œ๋น„์Šค์™€ Core Location ยท MapKit์œผ๋กœ ๋งŒ๋“œ๋Š” LBS ์•ฑ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ
๐Ÿ“ฑ ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ยท C

๐Ÿ—บ๏ธ iOS ์œ„์น˜ ์„œ๋น„์Šค์™€ Core Location ยท MapKit์œผ๋กœ ๋งŒ๋“œ๋Š” LBS ์•ฑ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ

Core Location๋ถ€ํ„ฐ MapKit ์ปค์Šคํ„ฐ๋งˆ์ด์ง•๊นŒ์ง€, ์‹ค์ „ LBS ์•ฑ ๊ฐœ๋ฐœ์˜ ๋ชจ๋“  ํ๋ฆ„์„ ํŒŒํ—ค์ณ๋ณด์ž!

์•ผ ์†”์งํžˆ ๋งํ•ด์„œ ๐Ÿ“ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์•ฑ(LBS, Location-Based Service)์€ ์ด์ œ ๊ทธ๋ƒฅ "์žˆ์œผ๋ฉด ์ข‹์€ ๊ธฐ๋Šฅ"์ด ์•„๋‹ˆ๋ผ ์—†์œผ๋ฉด ์•ฑ์ด ๋ฐ˜์ชฝ์งœ๋ฆฌ๊ฐ€ ๋˜๋Š” ์‹œ๋Œ€์ž–์•„ใ…‹ใ…‹ ๋ฐฐ๋‹ฌ ์•ฑ, ์นด์นด์˜ค๋งต, ๋‹น๊ทผ๋งˆ์ผ“, ์‹ฌ์ง€์–ด ํ”ผํŠธ๋‹ˆ์Šค ์•ฑ๊นŒ์ง€ ์ „๋ถ€ ์œ„์น˜ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋Œ์•„๊ฐ€๊ณ  ์žˆ์œผ๋‹ˆ๊นŒ. ๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ iOS์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํ•ต์‹ฌ ํ”„๋ ˆ์ž„์›Œํฌ์ธ Core Location๊ณผ MapKit์„ ์ œ๋Œ€๋กœ ๋œฏ์–ด๋ณด๋ ค๊ณ  ํ•ด!

C ์–ธ์–ด ๊ธฐ๋ฐ˜์˜ ์ €์ˆ˜์ค€ API๋ถ€ํ„ฐ Swift๋กœ ๊ฐ์‹ธ์ง„ ๊ณ ์ˆ˜์ค€ ์ธํ„ฐํŽ˜์ด์Šค๊นŒ์ง€, ์‹ค์ œ๋กœ ์•ฑ์—์„œ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์ฝ”๋“œ๋ž‘ ๊ฐœ๋…์„ ๊ฐ™์ด ๋ณด๋ฉด์„œ ๊ฐ€๋ณด์ž๊ณ ~ ๐Ÿš€


iOS LBS ์•ฑ ํ•ต์‹ฌ ๊ตฌ์กฐ ๐Ÿ“ก GPS / ์„ผ์„œ ์œ„์น˜ ์‹ ํ˜ธ ์ˆ˜์‹  ๐Ÿ”ต Core Location CLLocationManager ๊ถŒํ•œ ยท ์œ„์น˜ ยท ์ง€์˜คํŽœ์Šค ๐Ÿ—บ๏ธ MapKit MKMapView ์–ด๋…ธํ…Œ์ด์…˜ ยท ์˜ค๋ฒ„๋ ˆ์ด ๐Ÿ“ฑ LBS ์•ฑ ์™„์„ฑ! ๋ฐฐ๋‹ฌ ยท ์ง€๋„ ยท ์†Œ์…œ ํ”ผํŠธ๋‹ˆ์Šค ยท ์ฆ๊ฐ•ํ˜„์‹ค ๊ถŒํ•œ ์š”์ฒญ ํ๋ฆ„ ์•ฑ ์‹คํ–‰ โ†’ ๊ถŒํ•œ ํŒ์—… โ†’ ์œ„์น˜ ์ˆ˜์‹  ์‹œ์ž‘ ์ •ํ™•๋„ ๋ ˆ๋ฒจ Best / 10m / 100m ๋ฐฐํ„ฐ๋ฆฌ vs ์ •๋ฐ€๋„ ์ง€์˜คํŽœ์‹ฑ ์˜์—ญ ์ง„์ž…/์ดํƒˆ ๊ฐ์ง€ CLCircularRegion ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜ MKAnnotationView ๋งˆ์ปค ์ปค์Šคํ„ฐ๋งˆ์ด์ง• GPS ์‹ ํ˜ธ โ†’ Core Location ์ฒ˜๋ฆฌ โ†’ MapKit ์‹œ๊ฐํ™” โ†’ ์™„์„ฑ๋œ LBS ์•ฑ ๐ŸŽฏ ๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ ยท ์‚ฌ์šฉ์ž ํ”„๋ผ์ด๋ฒ„์‹œ ยท ์ •ํ™•๋„ ์„ธ ๋ฐ•์ž๋ฅผ ๋งž์ถ”๋Š” ๊ฒŒ ํ•ต์‹ฌ!

๐Ÿ“ก 1. LBS ์•ฑ์ด๋ž€? ์œ„์น˜ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค์˜ ์„ธ๊ณ„

LBS(Location-Based Service)๋Š” ๋ง ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ์œ„์น˜ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ์•ฑ์ด์•ผ. ์šฐ๋ฆฌ๊ฐ€ ๋งค์ผ ์“ฐ๋Š” ์•ฑ๋“ค ๋Œ€๋ถ€๋ถ„์ด ์—ฌ๊ธฐ์— ํ•ด๋‹นํ•ดใ…‹ใ…‹

๐ŸŒ LBS ์•ฑ์˜ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์‹œ๋“ค

๐Ÿ• ๋ฐฐ๋‹ฌ ์•ฑ ๐Ÿ—บ๏ธ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๐Ÿƒ ๋Ÿฌ๋‹ ํŠธ๋ž˜์ปค ๐Ÿ›’ ๊ทผ์ฒ˜ ๊ฐ€๊ฒŒ ์ฐพ๊ธฐ
๐Ÿš— ์นดํ’€/ํƒ์‹œ ๐Ÿ“ธ ์œ„์น˜ ํƒœ๊ทธ ์‚ฌ์ง„ ๐ŸŽฎ AR ๊ฒŒ์ž„ ๐Ÿฅ ์‘๊ธ‰ ์„œ๋น„์Šค

์ด ๋ชจ๋“  ์•ฑ๋“ค์ด iOS์—์„œ Core Location๊ณผ MapKit์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•ด!

iOS์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์“ฐ์ด๋Š” ํ•ต์‹ฌ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€์•ผ:

1
Core Location โ€” ์‹ค์ œ ์œ„์น˜ ๋ฐ์ดํ„ฐ(์œ„๋„, ๊ฒฝ๋„, ๊ณ ๋„, ์†๋„, ๋ฐฉํ–ฅ ๋“ฑ)๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ. GPS, Wi-Fi, ์…€๋ฃฐ๋Ÿฌ, ๋ธ”๋ฃจํˆฌ์Šค ๋“ฑ ๋‹ค์–‘ํ•œ ์‹ ํ˜ธ๋ฅผ ์ข…ํ•ฉํ•ด์„œ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ด์ค˜.
2
MapKit โ€” ์ง€๋„๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ณ , ์–ด๋…ธํ…Œ์ด์…˜(ํ•€), ์˜ค๋ฒ„๋ ˆ์ด(์„ /๋ฉด), ๊ฒฝ๋กœ ๋“ฑ์„ ์‹œ๊ฐํ™”ํ•˜๋Š” UI ํ”„๋ ˆ์ž„์›Œํฌ. Apple Maps ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•ด.
๐Ÿ’ก ์•Œ์•„๋‘๋ฉด ์ข‹์€ ๊ฒƒ!
Core Location์€ ์œ„์น˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์— ์ง‘์ค‘ํ•˜๊ณ , MapKit์€ ์ง€๋„ ์‹œ๊ฐํ™”์— ์ง‘์ค‘ํ•ด. ๋‘˜์€ ๋…๋ฆฝ์ ์œผ๋กœ๋„ ์“ธ ์ˆ˜ ์žˆ์–ด โ€” ์ง€๋„ ์—†์ด ์œ„์น˜๋งŒ ์ถ”์ ํ•˜๊ฑฐ๋‚˜, ์œ„์น˜ ์ถ”์  ์—†์ด ์ •์ ์ธ ์ง€๋„๋งŒ ๋ณด์—ฌ์ค„ ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ !

๐Ÿ”ต 2. Core Location ํ”„๋ ˆ์ž„์›Œํฌ ํŒŒํ—ค์น˜๊ธฐ

2-1. CLLocationManager โ€” ์œ„์น˜ ์„œ๋น„์Šค์˜ ์‹ฌ์žฅ

CLLocationManager๋Š” Core Location์˜ ํ•ต์‹ฌ ํด๋ž˜์Šค์•ผ. ์ด ์นœ๊ตฌ๊ฐ€ ์—†์œผ๋ฉด ์œ„์น˜ ์„œ๋น„์Šค ์ž์ฒด๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ดใ…‹ใ…‹ ์–˜๋ฅผ ํ†ตํ•ด์„œ ๊ถŒํ•œ ์š”์ฒญ, ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘/์ค‘์ง€, ์ง€์˜คํŽœ์‹ฑ ์„ค์ • ๋“ฑ ๋ชจ๋“  ๊ฑธ ์ œ์–ดํ•ด.

import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate {
    
    // CLLocationManager ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        // ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์„ค์ • - ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์„ ๊ฐ์ฒด ์ง€์ •
        locationManager.delegate = self
        // ์œ„์น˜ ์ •ํ™•๋„ ์„ค์ • (๋ฐฐํ„ฐ๋ฆฌ vs ์ •๋ฐ€๋„ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„)
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        // ์ตœ์†Œ ์ด๋™ ๊ฑฐ๋ฆฌ ์„ค์ • (๋ฏธํ„ฐ ๋‹จ์œ„) - ์ด ๊ฑฐ๋ฆฌ ์ด์ƒ ์›€์ง์—ฌ์•ผ ์—…๋ฐ์ดํŠธ
        locationManager.distanceFilter = 10.0
    }
    
    // ์œ„์น˜ ๊ถŒํ•œ ์š”์ฒญ
    func requestPermission() {
        locationManager.requestWhenInUseAuthorization()
    }
    
    // ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘
    func startUpdating() {
        locationManager.startUpdatingLocation()
    }
    
    // ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์ค‘์ง€
    func stopUpdating() {
        locationManager.stopUpdatingLocation()
    }
}

2-2. ์œ„์น˜ ๊ถŒํ•œ โ€” ์ด๊ฒŒ ์ œ์ผ ์ค‘์š”ํ•ด!

iOS์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ์“ฐ๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ถŒํ•œ์„ ์š”์ฒญํ•ด์•ผ ํ•ด. ์• ํ”Œ์ด ํ”„๋ผ์ด๋ฒ„์‹œ์— ์—„์ฒญ ์—„๊ฒฉํ•˜๊ฑฐ๋“ ใ…‹ใ…‹ ๊ถŒํ•œ ์ข…๋ฅ˜๋Š” ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€์•ผ:

๊ถŒํ•œ ์ข…๋ฅ˜ ์„ค๋ช… ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค
When In Use ์•ฑ์ด ํฌ๊ทธ๋ผ์šด๋“œ์— ์žˆ์„ ๋•Œ๋งŒ ์œ„์น˜ ์ ‘๊ทผ ์ง€๋„ ์•ฑ, ์Œ์‹ ์ฃผ๋ฌธ
Always ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ๋„ ์œ„์น˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ ๋Ÿฌ๋‹ ํŠธ๋ž˜์ปค, ์ง€์˜คํŽœ์‹ฑ
Denied/Restricted ์‚ฌ์šฉ์ž๊ฐ€ ๊ฑฐ๋ถ€ํ•˜๊ฑฐ๋‚˜ ์‹œ์Šคํ…œ ์ œํ•œ ์„ค์ • ์•ฑ์œผ๋กœ ์œ ๋„ ํ•„์š”
โš ๏ธ Info.plist ์„ค์ • ํ•„์ˆ˜!
๊ถŒํ•œ ์š”์ฒญ ์ „์— ๋ฐ˜๋“œ์‹œ Info.plist์— ์‚ฌ์šฉ ๋ชฉ์  ๋ฌธ์ž์—ด์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด. ์—†์œผ๋ฉด ์•ฑ์ด ํฌ๋ž˜์‹œ๋‚˜๊ฑฐ๋‚˜ ์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ์—์„œ ๋ฆฌ์ ๋‹นํ•ด!

NSLocationWhenInUseUsageDescription โ€” "์•ฑ ์‚ฌ์šฉ ์ค‘ ์œ„์น˜ ์ ‘๊ทผ ์ด์œ "
NSLocationAlwaysAndWhenInUseUsageDescription โ€” "ํ•ญ์ƒ ์œ„์น˜ ์ ‘๊ทผ ์ด์œ "
NSLocationAlwaysUsageDescription โ€” iOS 10 ์ดํ•˜ ํ˜ธํ™˜์šฉ
// ๊ถŒํ•œ ์ƒํƒœ ๋ณ€ํ™” ๊ฐ์ง€ (iOS 14+)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    switch manager.authorizationStatus {
    case .notDetermined:
        // ์•„์ง ๊ฒฐ์ • ์•ˆ ๋จ - ๊ถŒํ•œ ์š”์ฒญ
        manager.requestWhenInUseAuthorization()
    case .authorizedWhenInUse:
        // ์•ฑ ์‚ฌ์šฉ ์ค‘ ํ—ˆ์šฉ - ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘ ๊ฐ€๋Šฅ
        manager.startUpdatingLocation()
    case .authorizedAlways:
        // ํ•ญ์ƒ ํ—ˆ์šฉ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜๋„ ๊ฐ€๋Šฅ
        manager.startUpdatingLocation()
    case .denied:
        // ๊ฑฐ๋ถ€๋จ - ์„ค์ • ์•ฑ์œผ๋กœ ์œ ๋„
        print("์œ„์น˜ ๊ถŒํ•œ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„ค์ •์—์„œ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”.")
    case .restricted:
        // ์‹œ์Šคํ…œ ์ œํ•œ (์ž๋…€ ๋ณดํ˜ธ ๋“ฑ)
        print("์œ„์น˜ ์„œ๋น„์Šค๊ฐ€ ์ œํ•œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
    @unknown default:
        break
    }
}

2-3. CLLocation โ€” ์œ„์น˜ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด

CLLocation์€ ์‹ค์ œ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด์•ผ. ๋‹จ์ˆœํžˆ ์œ„๋„/๊ฒฝ๋„๋งŒ ์žˆ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์—„์ฒญ ๋งŽ์€ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด:

๐Ÿ“ CLLocation์ด ๋‹ด๊ณ  ์žˆ๋Š” ์ •๋ณด๋“ค

coordinate.latitude ์œ„๋„ (-90 ~ 90)
coordinate.longitude ๊ฒฝ๋„ (-180 ~ 180)
altitude ๊ณ ๋„ (๋ฏธํ„ฐ)
horizontalAccuracy ์ˆ˜ํ‰ ์ •ํ™•๋„ (๋ฏธํ„ฐ, ์ž‘์„์ˆ˜๋ก ์ •ํ™•)
verticalAccuracy ์ˆ˜์ง ์ •ํ™•๋„ (๋ฏธํ„ฐ)
speed ์†๋„ (m/s)
course ์ด๋™ ๋ฐฉํ–ฅ (0~360๋„, ๋ถ์ชฝ์ด 0)
timestamp ์ธก์ • ์‹œ๊ฐ
// ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์ˆ˜์‹  ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฉ”์„œ๋“œ
func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    // ๊ฐ€์žฅ ์ตœ๊ทผ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ
    guard let location = locations.last else { return }
    
    let latitude = location.coordinate.latitude
    let longitude = location.coordinate.longitude
    let accuracy = location.horizontalAccuracy
    let speed = location.speed  // m/s
    let altitude = location.altitude
    
    // ์ •ํ™•๋„ ํ•„ํ„ฐ๋ง - ๋„ˆ๋ฌด ๋ถ€์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋ฌด์‹œ
    // horizontalAccuracy๊ฐ€ ์Œ์ˆ˜๋ฉด ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ
    guard accuracy > 0 && accuracy < 100 else { return }
    
    print("์œ„์น˜: \(latitude), \(longitude)")
    print("์ •ํ™•๋„: \(accuracy)m")
    print("์†๋„: \(speed * 3.6)km/h")  // m/s โ†’ km/h ๋ณ€ํ™˜
}

// ์œ„์น˜ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
func locationManager(_ manager: CLLocationManager,
                     didFailWithError error: Error) {
    if let clError = error as? CLError {
        switch clError.code {
        case .denied:
            print("์œ„์น˜ ์ ‘๊ทผ ๊ฑฐ๋ถ€๋จ")
        case .locationUnknown:
            print("์œ„์น˜๋ฅผ ์ผ์‹œ์ ์œผ๋กœ ์•Œ ์ˆ˜ ์—†์Œ")
        default:
            print("์œ„์น˜ ์˜ค๋ฅ˜: \(clError.localizedDescription)")
        }
    }
}

2-4. ์œ„์น˜ ์ •ํ™•๋„ ์„ค์ • โ€” ๋ฐฐํ„ฐ๋ฆฌ์™€์˜ ํƒ€ํ˜‘

์œ„์น˜ ์ •ํ™•๋„๋Š” ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ์™€ ์ง๊ฒฐ๋ผ. ์ •ํ™•ํ• ์ˆ˜๋ก ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ๋งŽ์ด ๋จน์–ดใ…‹ใ…‹ ์•ฑ์˜ ์šฉ๋„์— ๋งž๊ฒŒ ์ ์ ˆํžˆ ์„ ํƒํ•ด์•ผ ํ•ด:

์ •ํ™•๋„ ์ƒ์ˆ˜ ์˜ค์ฐจ ๋ฒ”์œ„ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ถ”์ฒœ ์šฉ๋„
kCLLocationAccuracyBestForNavigation ~1m ๋งค์šฐ ๋†’์Œ ๋‚ด๋น„๊ฒŒ์ด์…˜, AR
kCLLocationAccuracyBest ~5m ๋†’์Œ ๋Ÿฌ๋‹ ํŠธ๋ž˜์ปค
kCLLocationAccuracyNearestTenMeters ~10m ์ค‘๊ฐ„ ์ผ๋ฐ˜ ์ง€๋„ ์•ฑ
kCLLocationAccuracyHundredMeters ~100m ๋‚ฎ์Œ ๋‚ ์”จ, ์ง€์—ญ ์ •๋ณด
kCLLocationAccuracyKilometer ~1km ๋งค์šฐ ๋‚ฎ์Œ ๋„์‹œ ์ˆ˜์ค€ ์œ„์น˜
kCLLocationAccuracyThreeKilometers ~3km ์ตœ์†Œ ๊ด‘์—ญ ์ง€์—ญ ์„œ๋น„์Šค

2-5. ์ง€์˜คํŽœ์‹ฑ โ€” ์˜์—ญ ์ง„์ž…/์ดํƒˆ ๊ฐ์ง€

์ง€์˜คํŽœ์‹ฑ(Geofencing)์€ ํŠน์ • ์ง€๋ฆฌ์  ์˜์—ญ์„ ์„ค์ •ํ•ด๋‘๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ์˜์—ญ์— ์ง„์ž…ํ•˜๊ฑฐ๋‚˜ ์ดํƒˆํ•  ๋•Œ ์•Œ๋ฆผ์„ ๋ฐ›๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ํŽธ์˜์  ์•ฑ์—์„œ "๊ทผ์ฒ˜์— ์™”์–ด์š”!" ์•Œ๋ฆผ ๊ฐ™์€ ๊ฑฐใ…‹ใ…‹

// ์ง€์˜คํŽœ์Šค ์˜์—ญ ์„ค์ •
func setupGeofence() {
    // ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ค‘์‹ฌ ์ขŒํ‘œ (์˜ˆ: ์„œ์šธ ์‹œ์ฒญ)
    let center = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
    
    // ์›ํ˜• ์˜์—ญ ์ƒ์„ฑ (๋ฐ˜๊ฒฝ 200๋ฏธํ„ฐ)
    let region = CLCircularRegion(
        center: center,
        radius: 200.0,  // ๋ฏธํ„ฐ ๋‹จ์œ„
        identifier: "์„œ์šธ์‹œ์ฒญ_์˜์—ญ"
    )
    
    // ์ง„์ž…/์ดํƒˆ ์ด๋ฒคํŠธ ์„ค์ •
    region.notifyOnEntry = true   // ์ง„์ž… ์‹œ ์•Œ๋ฆผ
    region.notifyOnExit = true    // ์ดํƒˆ ์‹œ ์•Œ๋ฆผ
    
    // ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
    locationManager.startMonitoring(for: region)
}

// ์˜์—ญ ์ง„์ž… ๊ฐ์ง€
func locationManager(_ manager: CLLocationManager,
                     didEnterRegion region: CLRegion) {
    print("์˜์—ญ ์ง„์ž…: \(region.identifier)")
    // ํ‘ธ์‹œ ์•Œ๋ฆผ ๋ฐœ์†ก ๋“ฑ์˜ ๋กœ์ง
    sendLocalNotification(title: "๋„์ฐฉ!", body: "์„œ์šธ์‹œ์ฒญ ๊ทผ์ฒ˜์— ๋„์ฐฉํ–ˆ์–ด์š”!")
}

// ์˜์—ญ ์ดํƒˆ ๊ฐ์ง€
func locationManager(_ manager: CLLocationManager,
                     didExitRegion region: CLRegion) {
    print("์˜์—ญ ์ดํƒˆ: \(region.identifier)")
}

// ์ง€์˜คํŽœ์Šค ์ œํ•œ์‚ฌํ•ญ
// - ๊ธฐ๊ธฐ๋‹น ์ตœ๋Œ€ 20๊ฐœ ์˜์—ญ ๋™์‹œ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€๋Šฅ
// - ์ตœ์†Œ ๋ฐ˜๊ฒฝ: 100๋ฏธํ„ฐ (์‹ค์ œ๋กœ๋Š” ๋” ํด ์ˆ˜ ์žˆ์Œ)
// - Always ๊ถŒํ•œ ํ•„์š” (๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™์ž‘)
๐Ÿ’ก ์ง€์˜คํŽœ์‹ฑ ๊ฟ€ํŒ!
์ง€์˜คํŽœ์‹ฑ์€ GPS๋งŒ ์“ฐ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ Wi-Fi, ์…€๋ฃฐ๋Ÿฌ ์‹ ํ˜ธ๋„ ํ™œ์šฉํ•ด์„œ ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์จ. ๊ทธ๋ž˜์„œ Always ๊ถŒํ•œ์ด ์žˆ์–ด๋„ ์ƒ๊ฐ๋ณด๋‹ค ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ๋งŽ์ด ์•ˆ ๋จน์–ด! ๋‹จ, ๊ธฐ๊ธฐ๋‹น ์ตœ๋Œ€ 20๊ฐœ ์˜์—ญ ์ œํ•œ์ด ์žˆ์œผ๋‹ˆ๊นŒ ์šฐ์„ ์ˆœ์œ„ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•ด.

๐Ÿ—บ๏ธ 3. MapKit ํ”„๋ ˆ์ž„์›Œํฌ โ€” ์ง€๋„๋ฅผ ๋‚ด ์†์œผ๋กœ!

3-1. MKMapView ๊ธฐ๋ณธ ์„ค์ •

MapKit์˜ ํ•ต์‹ฌ์€ MKMapView์•ผ. ์ด ๋ทฐ ํ•˜๋‚˜๋กœ Apple Maps๋ฅผ ์•ฑ์— ํ†ต์งธ๋กœ ์ง‘์–ด๋„ฃ์„ ์ˆ˜ ์žˆ์–ดใ…‹ใ…‹ ์Šคํ† ๋ฆฌ๋ณด๋“œ๋‚˜ ์ฝ”๋“œ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.

import MapKit

class MapViewController: UIViewController, MKMapViewDelegate {
    
    @IBOutlet weak var mapView: MKMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์„ค์ •
        mapView.delegate = self
        
        // ์ง€๋„ ํƒ€์ž… ์„ค์ •
        mapView.mapType = .standard        // ๊ธฐ๋ณธ ์ง€๋„
        // mapView.mapType = .satellite    // ์œ„์„ฑ ์‚ฌ์ง„
        // mapView.mapType = .hybrid       // ์œ„์„ฑ + ๋„๋กœ
        // mapView.mapType = .hybridFlyover // 3D ์œ„์„ฑ
        
        // ์‚ฌ์šฉ์ž ์œ„์น˜ ํ‘œ์‹œ (ํŒŒ๋ž€ ์ )
        mapView.showsUserLocation = true
        
        // ๋‚˜์นจ๋ฐ˜ ํ‘œ์‹œ
        mapView.showsCompass = true
        
        // ์ถ•์ฒ™ ํ‘œ์‹œ
        mapView.showsScale = true
        
        // ๊ตํ†ต ์ •๋ณด ํ‘œ์‹œ
        mapView.showsTraffic = true
        
        // ๊ฑด๋ฌผ 3D ํ‘œ์‹œ
        mapView.showsBuildings = true
        
        // ์ดˆ๊ธฐ ์ง€๋„ ์˜์—ญ ์„ค์ • (์„œ์šธ ์ค‘์‹ฌ)
        let seoulCenter = CLLocationCoordinate2D(
            latitude: 37.5665,
            longitude: 126.9780
        )
        let span = MKCoordinateSpan(
            latitudeDelta: 0.05,   // ์œ„๋„ ๋ฒ”์œ„ (์ž‘์„์ˆ˜๋ก ํ™•๋Œ€)
            longitudeDelta: 0.05   // ๊ฒฝ๋„ ๋ฒ”์œ„
        )
        let region = MKCoordinateRegion(center: seoulCenter, span: span)
        mapView.setRegion(region, animated: true)
    }
}

3-2. ์–ด๋…ธํ…Œ์ด์…˜ โ€” ์ง€๋„ ์œ„์˜ ํ•€ ๊ฝ‚๊ธฐ

์–ด๋…ธํ…Œ์ด์…˜(Annotation)์€ ์ง€๋„ ์œ„์— ํŠน์ • ์œ„์น˜๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋งˆ์ปค์•ผ. ์นด์นด์˜ค๋งต์—์„œ ๊ฐ€๊ฒŒ ์œ„์น˜ ํ‘œ์‹œํ•˜๋Š” ๊ทธ ํ•€๋“ค์ด ๋‹ค ์–ด๋…ธํ…Œ์ด์…˜์ด์•ผใ…‹ใ…‹

// ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜ ๋ชจ๋ธ ์ •์˜
class CustomAnnotation: NSObject, MKAnnotation {
    var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var category: String  // ์นดํ…Œ๊ณ ๋ฆฌ (์นดํŽ˜, ์‹๋‹น ๋“ฑ)
    
    init(coordinate: CLLocationCoordinate2D,
         title: String,
         subtitle: String,
         category: String) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.category = category
    }
}

// ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€
func addAnnotations() {
    let places = [
        ("์Šคํƒ€๋ฒ…์Šค ๊ฐ•๋‚จ์ ", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ", 37.4979, 127.0276, "์นดํŽ˜"),
        ("๋งฅ๋„๋‚ ๋“œ ํ™๋Œ€์ ", "์„œ์šธ ๋งˆํฌ๊ตฌ", 37.5563, 126.9236, "ํŒจ์ŠคํŠธํ‘ธ๋“œ"),
        ("๊ตญ๋ฆฝ์ค‘์•™๋ฐ•๋ฌผ๊ด€", "์„œ์šธ ์šฉ์‚ฐ๊ตฌ", 37.5236, 126.9803, "๋ฌธํ™”")
    ]
    
    for place in places {
        let annotation = CustomAnnotation(
            coordinate: CLLocationCoordinate2D(
                latitude: place.2,
                longitude: place.3
            ),
            title: place.0,
            subtitle: place.1,
            category: place.4
        )
        mapView.addAnnotation(annotation)
    }
}

// ์–ด๋…ธํ…Œ์ด์…˜ ๋ทฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
func mapView(_ mapView: MKMapView,
             viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    // ์‚ฌ์šฉ์ž ์œ„์น˜ ์–ด๋…ธํ…Œ์ด์…˜์€ ๊ธฐ๋ณธ ๋ทฐ ์‚ฌ์šฉ
    guard let customAnnotation = annotation as? CustomAnnotation else {
        return nil
    }
    
    let identifier = "CustomPin"
    var annotationView = mapView.dequeueReusableAnnotationView(
        withIdentifier: identifier
    ) as? MKMarkerAnnotationView
    
    if annotationView == nil {
        annotationView = MKMarkerAnnotationView(
            annotation: customAnnotation,
            reuseIdentifier: identifier
        )
        annotationView?.canShowCallout = true  // ํƒญ ์‹œ ๋งํ’์„  ํ‘œ์‹œ
    } else {
        annotationView?.annotation = customAnnotation
    }
    
    // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒ‰์ƒ ์„ค์ •
    switch customAnnotation.category {
    case "์นดํŽ˜":
        annotationView?.markerTintColor = .brown
        annotationView?.glyphText = "โ˜•"
    case "ํŒจ์ŠคํŠธํ‘ธ๋“œ":
        annotationView?.markerTintColor = .red
        annotationView?.glyphText = "๐Ÿ”"
    case "๋ฌธํ™”":
        annotationView?.markerTintColor = .purple
        annotationView?.glyphText = "๐Ÿ›๏ธ"
    default:
        annotationView?.markerTintColor = .blue
    }
    
    return annotationView
}

3-3. ์˜ค๋ฒ„๋ ˆ์ด โ€” ์„ ๊ณผ ๋ฉด์œผ๋กœ ์ง€๋„ ๊พธ๋ฏธ๊ธฐ

์˜ค๋ฒ„๋ ˆ์ด(Overlay)๋Š” ์ง€๋„ ์œ„์— ์„ , ์›, ๋‹ค๊ฐํ˜• ๋“ฑ์„ ๊ทธ๋ฆฌ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๊ฒฝ๋กœ ํ‘œ์‹œ๋‚˜ ํŠน์ • ๊ตฌ์—ญ ๊ฐ•์กฐํ•  ๋•Œ ์“ฐ์—ฌ:

// ์›ํ˜• ์˜ค๋ฒ„๋ ˆ์ด (์ง€์˜คํŽœ์Šค ์‹œ๊ฐํ™”)
func addCircleOverlay() {
    let center = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
    let circle = MKCircle(center: center, radius: 500)  // ๋ฐ˜๊ฒฝ 500m
    mapView.addOverlay(circle)
}

// ๊ฒฝ๋กœ ์„  ์˜ค๋ฒ„๋ ˆ์ด
func addPolylineOverlay() {
    var coordinates = [
        CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780),
        CLLocationCoordinate2D(latitude: 37.5700, longitude: 126.9820),
        CLLocationCoordinate2D(latitude: 37.5750, longitude: 126.9900)
    ]
    let polyline = MKPolyline(coordinates: &coordinates, count: coordinates.count)
    mapView.addOverlay(polyline)
}

// ๋‹ค๊ฐํ˜• ์˜ค๋ฒ„๋ ˆ์ด
func addPolygonOverlay() {
    var coordinates = [
        CLLocationCoordinate2D(latitude: 37.56, longitude: 126.97),
        CLLocationCoordinate2D(latitude: 37.57, longitude: 126.97),
        CLLocationCoordinate2D(latitude: 37.57, longitude: 126.99),
        CLLocationCoordinate2D(latitude: 37.56, longitude: 126.99)
    ]
    let polygon = MKPolygon(coordinates: &coordinates, count: coordinates.count)
    mapView.addOverlay(polygon)
}

// ์˜ค๋ฒ„๋ ˆ์ด ๋ Œ๋”๋Ÿฌ ์„ค์ • (๋ธ๋ฆฌ๊ฒŒ์ดํŠธ)
func mapView(_ mapView: MKMapView,
             rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if let circle = overlay as? MKCircle {
        let renderer = MKCircleRenderer(circle: circle)
        renderer.fillColor = UIColor.blue.withAlphaComponent(0.1)
        renderer.strokeColor = .blue
        renderer.lineWidth = 2
        return renderer
    }
    if let polyline = overlay as? MKPolyline {
        let renderer = MKPolylineRenderer(polyline: polyline)
        renderer.strokeColor = .systemBlue
        renderer.lineWidth = 4
        renderer.lineDashPattern = [8, 4]  // ์ ์„  ํŒจํ„ด
        return renderer
    }
    if let polygon = overlay as? MKPolygon {
        let renderer = MKPolygonRenderer(polygon: polygon)
        renderer.fillColor = UIColor.green.withAlphaComponent(0.15)
        renderer.strokeColor = .green
        renderer.lineWidth = 2
        return renderer
    }
    return MKOverlayRenderer(overlay: overlay)
}

3-4. ์—ญ์ง€์˜ค์ฝ”๋”ฉ โ€” ์ขŒํ‘œ๋ฅผ ์ฃผ์†Œ๋กœ!

์—ญ์ง€์˜ค์ฝ”๋”ฉ(Reverse Geocoding)์€ ์œ„๋„/๊ฒฝ๋„ ์ขŒํ‘œ๋ฅผ ์‚ฌ๋žŒ์ด ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๋ฐ˜๋Œ€๋กœ ์ฃผ์†Œ๋ฅผ ์ขŒํ‘œ๋กœ ๋ฐ”๊พธ๋Š” ๊ฑด ์ง€์˜ค์ฝ”๋”ฉ(Geocoding)์ด๋ผ๊ณ  ํ•ด:

// ์—ญ์ง€์˜ค์ฝ”๋”ฉ - ์ขŒํ‘œ โ†’ ์ฃผ์†Œ
func reverseGeocode(location: CLLocation) {
    let geocoder = CLGeocoder()
    
    geocoder.reverseGeocodeLocation(location) { placemarks, error in
        if let error = error {
            print("์—ญ์ง€์˜ค์ฝ”๋”ฉ ์‹คํŒจ: \(error.localizedDescription)")
            return
        }
        
        guard let placemark = placemarks?.first else { return }
        
        // ์ฃผ์†Œ ๊ตฌ์„ฑ ์š”์†Œ
        let country = placemark.country ?? ""           // ๊ตญ๊ฐ€: ๋Œ€ํ•œ๋ฏผ๊ตญ
        let administrativeArea = placemark.administrativeArea ?? ""  // ์‹œ/๋„: ์„œ์šธํŠน๋ณ„์‹œ
        let locality = placemark.locality ?? ""         // ์‹œ/๊ตฐ/๊ตฌ: ์ค‘๊ตฌ
        let subLocality = placemark.subLocality ?? ""   // ๋™/์/๋ฉด: ๋ช…๋™
        let thoroughfare = placemark.thoroughfare ?? "" // ๋„๋กœ๋ช…
        let subThoroughfare = placemark.subThoroughfare ?? "" // ๋ฒˆ์ง€
        
        let address = "\(country) \(administrativeArea) \(locality) \(subLocality)"
        print("์ฃผ์†Œ: \(address)")
    }
}

// ์ง€์˜ค์ฝ”๋”ฉ - ์ฃผ์†Œ โ†’ ์ขŒํ‘œ
func geocodeAddress(address: String) {
    let geocoder = CLGeocoder()
    
    geocoder.geocodeAddressString(address) { placemarks, error in
        if let error = error {
            print("์ง€์˜ค์ฝ”๋”ฉ ์‹คํŒจ: \(error.localizedDescription)")
            return
        }
        
        guard let placemark = placemarks?.first,
              let location = placemark.location else { return }
        
        let coordinate = location.coordinate
        print("์ขŒํ‘œ: \(coordinate.latitude), \(coordinate.longitude)")
        
        // ์ง€๋„์— ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€
        let annotation = MKPointAnnotation()
        annotation.coordinate = coordinate
        annotation.title = address
        self.mapView.addAnnotation(annotation)
    }
}

LBS ์•ฑ ๊ฐœ๋ฐœ ํ•ต์‹ฌ ํ๋ฆ„๋„ โ‘  ๊ถŒํ•œ ์š”์ฒญ requestWhenInUse Authorization() Info.plist ์„ค์ • ํ•„์ˆ˜! โ‘ก ์œ„์น˜ ์ˆ˜์‹  startUpdating Location() CLLocation ๊ฐ์ฒด ์ˆ˜์‹  โ‘ข ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ขŒํ‘œ ํ•„ํ„ฐ๋ง ์—ญ์ง€์˜ค์ฝ”๋”ฉ API ํ˜ธ์ถœ ๋“ฑ โ‘ฃ ์ง€๋„ ํ‘œ์‹œ MKMapView ์–ด๋…ธํ…Œ์ด์…˜/์˜ค๋ฒ„๋ ˆ์ด ์ปค์Šคํ…€ ๋งˆ์ปค ํ‘œ์‹œ โ‘ค UX ์•Œ๋ฆผ ๋ฐœ์†ก ์„œ๋น„์Šค ์ œ๊ณต ์‚ฌ์šฉ์ž ๊ฒฝํ—˜! ๐Ÿ”‘ ํ•ต์‹ฌ ๊ณ ๋ ค์‚ฌํ•ญ ๐Ÿ”‹ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” distanceFilter ์„ค์ • ์ ์ ˆํ•œ ์ •ํ™•๋„ ์„ ํƒ ๋ถˆํ•„์š”์‹œ ์—…๋ฐ์ดํŠธ ์ค‘์ง€ ๐Ÿ”’ ํ”„๋ผ์ด๋ฒ„์‹œ ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ ์‚ฌ์šฉ ๋ชฉ์  ๋ช…ํ™•ํžˆ ๊ณ ์ง€ ๋ฐ์ดํ„ฐ ์ตœ์†Œ ์ˆ˜์ง‘ ๐Ÿ“ก ์‹ ํ˜ธ ๋ถˆ์•ˆ์ • ๋Œ€์‘ ์ •ํ™•๋„ ํ•„ํ„ฐ๋ง ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋กœ์ง ์บ์‹œ ํ™œ์šฉ ๐Ÿงช ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ…Œ์ŠคํŠธ Xcode ์œ„์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ GPX ํŒŒ์ผ๋กœ ๊ฒฝ๋กœ ํ…Œ์ŠคํŠธ ์‹ค๊ธฐ๊ธฐ ์ตœ์ข… ๊ฒ€์ฆ ํ•„์ˆ˜ ๋ฐฐํ„ฐ๋ฆฌ ๐Ÿ”‹ ยท ํ”„๋ผ์ด๋ฒ„์‹œ ๐Ÿ”’ ยท ์ •ํ™•๋„ ๐Ÿ“ โ€” ์„ธ ๊ฐ€์ง€ ๊ท ํ˜•์ด LBS ์•ฑ์˜ ํ•ต์‹ฌ! ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ•ด์น˜์ง€ ์•Š์œผ๋ฉด์„œ ํ•„์š”ํ•œ ์œ„์น˜ ์ •๋ณด๋งŒ ์ˆ˜์ง‘ํ•˜๋Š” ๊ฒƒ์ด ์ตœ์„ ์˜ ์„ค๊ณ„์•ผ ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์žฌ๋Šฅ ๋งค์นญ ์„œ๋น„์Šค ๊ฐ™์€ ๊ฑธ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒ ์ง€? ๐Ÿ˜„

๐Ÿš€ 4. ์‹ค์ „ LBS ์•ฑ ๊ตฌํ˜„ โ€” ๊ทผ์ฒ˜ ์žฅ์†Œ ์ฐพ๊ธฐ ์•ฑ

์ด์ œ ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์˜ ์ฝ”๋“œ๋ฅผ ๋ณด์ž! ํ˜„์žฌ ์œ„์น˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ทผ์ฒ˜ ์žฅ์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์ง€๋„์— ํ‘œ์‹œํ•˜๋Š” ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒใ…‹ใ…‹

4-1. MKLocalSearch โ€” ์ฃผ๋ณ€ ์žฅ์†Œ ๊ฒ€์ƒ‰

MapKit์—๋Š” MKLocalSearch๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ๊ฒ€์ƒ‰ API๊ฐ€ ๋‚ด์žฅ๋ผ ์žˆ์–ด. ๋ณ„๋„์˜ ์„œ๋ฒ„ ์—†์ด๋„ Apple Maps ๋ฐ์ดํ„ฐ๋กœ ์ฃผ๋ณ€ ์žฅ์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด:

// ํ˜„์žฌ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ฃผ๋ณ€ ์นดํŽ˜ ๊ฒ€์ƒ‰
func searchNearbyPlaces(coordinate: CLLocationCoordinate2D, query: String) {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = query  // "์นดํŽ˜", "ํŽธ์˜์ ", "์ฃผ์œ ์†Œ" ๋“ฑ
    
    // ๊ฒ€์ƒ‰ ๋ฐ˜๊ฒฝ ์„ค์ • (ํ˜„์žฌ ์œ„์น˜ ์ค‘์‹ฌ 1km ๋ฐ˜๊ฒฝ)
    let region = MKCoordinateRegion(
        center: coordinate,
        latitudinalMeters: 1000,
        longitudinalMeters: 1000
    )
    request.region = region
    
    // ๊ฒฐ๊ณผ ํƒ€์ž… ํ•„ํ„ฐ (iOS 13+)
    request.resultTypes = [.pointOfInterest, .address]
    
    let search = MKLocalSearch(request: request)
    search.start { response, error in
        if let error = error {
            print("๊ฒ€์ƒ‰ ์‹คํŒจ: \(error.localizedDescription)")
            return
        }
        
        guard let response = response else { return }
        
        // ๊ธฐ์กด ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ
        self.mapView.removeAnnotations(self.mapView.annotations)
        
        // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
        for item in response.mapItems {
            let annotation = MKPointAnnotation()
            annotation.coordinate = item.placemark.coordinate
            annotation.title = item.name
            annotation.subtitle = item.placemark.thoroughfare
            self.mapView.addAnnotation(annotation)
            
            // ์ถ”๊ฐ€ ์ •๋ณด
            print("์žฅ์†Œ๋ช…: \(item.name ?? "")")
            print("์ „ํ™”๋ฒˆํ˜ธ: \(item.phoneNumber ?? "")")
            print("URL: \(item.url?.absoluteString ?? "")")
            print("์นดํ…Œ๊ณ ๋ฆฌ: \(item.pointOfInterestCategory?.rawValue ?? "")")
        }
        
        // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋ณด์ด๋„๋ก ์ง€๋„ ์˜์—ญ ์กฐ์ •
        self.mapView.showAnnotations(self.mapView.annotations, animated: true)
    }
}

4-2. ๊ฒฝ๋กœ ์•ˆ๋‚ด โ€” MKDirections

์ถœ๋ฐœ์ง€์—์„œ ๋ชฉ์ ์ง€๊นŒ์ง€์˜ ๊ฒฝ๋กœ๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ์ง€๋„์— ํ‘œ์‹œํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๋‚ด๋น„๊ฒŒ์ด์…˜ ์•ฑ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด์ง€ใ…‹ใ…‹

// ๊ฒฝ๋กœ ๊ณ„์‚ฐ ๋ฐ ํ‘œ์‹œ
func calculateRoute(from source: CLLocationCoordinate2D,
                    to destination: CLLocationCoordinate2D) {
    // ์ถœ๋ฐœ์ง€ ์„ค์ •
    let sourcePlacemark = MKPlacemark(coordinate: source)
    let sourceMapItem = MKMapItem(placemark: sourcePlacemark)
    
    // ๋ชฉ์ ์ง€ ์„ค์ •
    let destinationPlacemark = MKPlacemark(coordinate: destination)
    let destinationMapItem = MKMapItem(placemark: destinationPlacemark)
    
    // ๊ฒฝ๋กœ ์š”์ฒญ ์„ค์ •
    let request = MKDirections.Request()
    request.source = sourceMapItem
    request.destination = destinationMapItem
    request.transportType = .automobile  // ์ž๋™์ฐจ ๊ฒฝ๋กœ
    // .walking  โ€” ๋„๋ณด
    // .transit  โ€” ๋Œ€์ค‘๊ตํ†ต
    // .any      โ€” ๋ชจ๋“  ์ˆ˜๋‹จ
    request.requestsAlternateRoutes = true  // ๋Œ€์•ˆ ๊ฒฝ๋กœ๋„ ์š”์ฒญ
    
    let directions = MKDirections(request: request)
    directions.calculate { response, error in
        if let error = error {
            print("๊ฒฝ๋กœ ๊ณ„์‚ฐ ์‹คํŒจ: \(error.localizedDescription)")
            return
        }
        
        guard let response = response else { return }
        
        // ๊ธฐ์กด ์˜ค๋ฒ„๋ ˆ์ด ์ œ๊ฑฐ
        self.mapView.removeOverlays(self.mapView.overlays)
        
        // ์ฒซ ๋ฒˆ์งธ ๊ฒฝ๋กœ (์ตœ์  ๊ฒฝ๋กœ) ํ‘œ์‹œ
        if let route = response.routes.first {
            self.mapView.addOverlay(route.polyline, level: .aboveRoads)
            
            // ๊ฒฝ๋กœ ์ •๋ณด ์ถœ๋ ฅ
            let distance = route.distance / 1000  // km
            let time = route.expectedTravelTime / 60  // ๋ถ„
            print("๊ฑฐ๋ฆฌ: \(String(format: "%.1f", distance))km")
            print("์˜ˆ์ƒ ์‹œ๊ฐ„: \(Int(time))๋ถ„")
            
            // ๊ฒฝ๋กœ๊ฐ€ ๋ณด์ด๋„๋ก ์ง€๋„ ์กฐ์ •
            let rect = route.polyline.boundingMapRect
            self.mapView.setVisibleMapRect(
                rect,
                edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50),
                animated: true
            )
            
            // ํ„ด๋ฐ”์ดํ„ด ์•ˆ๋‚ด ์ •๋ณด
            for step in route.steps {
                print("์•ˆ๋‚ด: \(step.instructions)")
                print("๊ฑฐ๋ฆฌ: \(step.distance)m")
            }
        }
    }
}

// ๊ฒฝ๋กœ ์˜ค๋ฒ„๋ ˆ์ด ๋ Œ๋”๋Ÿฌ (๋ธ๋ฆฌ๊ฒŒ์ดํŠธ์— ์ถ”๊ฐ€)
// func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
//     if let polyline = overlay as? MKPolyline {
//         let renderer = MKPolylineRenderer(polyline: polyline)
//         renderer.strokeColor = .systemBlue
//         renderer.lineWidth = 5
//         return renderer
//     }
//     return MKOverlayRenderer(overlay: overlay)
// }

4-3. ์‚ฌ์šฉ์ž ์œ„์น˜ ์ถ”์  ๋ฐ ์ง€๋„ ์ค‘์‹ฌ ์ด๋™

// ์‚ฌ์šฉ์ž ์œ„์น˜ ์ถ”์  ๋ชจ๋“œ ์„ค์ •
func setupUserTracking() {
    // ์‚ฌ์šฉ์ž ์œ„์น˜๋ฅผ ์ง€๋„ ์ค‘์‹ฌ์œผ๋กœ ์ž๋™ ์ถ”์ 
    mapView.userTrackingMode = .follow
    // .none          โ€” ์ถ”์  ์•ˆ ํ•จ
    // .follow        โ€” ์œ„์น˜ ์ถ”์  (๋ฐฉํ–ฅ ๊ณ ์ •)
    // .followWithHeading โ€” ์œ„์น˜ + ๋ฐฉํ–ฅ ์ถ”์  (๋‚˜์นจ๋ฐ˜ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „)
}

// ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์‹œ ์ง€๋„ ์ค‘์‹ฌ ์ด๋™
func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last,
          location.horizontalAccuracy > 0 else { return }
    
    // ์ฒ˜์Œ ์œ„์น˜ ์ˆ˜์‹  ์‹œ์—๋งŒ ์ง€๋„ ์ค‘์‹ฌ ์ด๋™
    if isFirstLocationUpdate {
        let region = MKCoordinateRegion(
            center: location.coordinate,
            latitudinalMeters: 500,
            longitudinalMeters: 500
        )
        mapView.setRegion(region, animated: true)
        isFirstLocationUpdate = false
    }
    
    // ํ˜„์žฌ ์œ„์น˜ ์ €์žฅ
    currentLocation = location
    
    // ์ด๋™ ๊ฒฝ๋กœ ๊ธฐ๋ก (๋Ÿฌ๋‹ ์•ฑ ๋“ฑ์—์„œ ํ™œ์šฉ)
    routeCoordinates.append(location.coordinate)
    updateRouteOverlay()
}

// ์ด๋™ ๊ฒฝ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
private var routeCoordinates: [CLLocationCoordinate2D] = []

func updateRouteOverlay() {
    // ๊ธฐ์กด ๊ฒฝ๋กœ ์˜ค๋ฒ„๋ ˆ์ด ์ œ๊ฑฐ
    let existingOverlays = mapView.overlays.filter { $0 is MKPolyline }
    mapView.removeOverlays(existingOverlays)
    
    // ์ƒˆ ๊ฒฝ๋กœ ์˜ค๋ฒ„๋ ˆ์ด ์ถ”๊ฐ€
    if routeCoordinates.count >= 2 {
        var coords = routeCoordinates
        let polyline = MKPolyline(coordinates: &coords, count: coords.count)
        mapView.addOverlay(polyline, level: .aboveRoads)
    }
}

โšก 5. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์—…๋ฐ์ดํŠธ โ€” ์•ฑ์ด ๊บผ์ ธ๋„ ์ถ”์ !

๋Ÿฌ๋‹ ์•ฑ์ด๋‚˜ ๋ฐฐ๋‹ฌ ์•ฑ์ฒ˜๋Ÿผ ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ์žˆ์–ด๋„ ์œ„์น˜๋ฅผ ๊ณ„์† ์ถ”์ ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด. ์ด๊ฑด ์„ค์ •์ด ์ข€ ๋ณต์žกํ•ดใ…‹ใ…‹

โš ๏ธ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ!
๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋Š” ์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ์—์„œ ๋งค์šฐ ์—„๊ฒฉํ•˜๊ฒŒ ๊ฒ€ํ† ๋ผ. ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ์‚ฌ์šฉ ๋ชฉ์ ์„ ๋ช…ํ™•ํžˆ ์„ค๋ช…ํ•ด์•ผ ํ•ด. ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ๋ฆฌ์ ๋‹นํ•˜๊ฑฐ๋‚˜ ์•ฑ์ด ์‚ญ์ œ๋  ์ˆ˜ ์žˆ์–ด!
// 1. Xcode ํ”„๋กœ์ ํŠธ ์„ค์ •
// Target โ†’ Signing & Capabilities โ†’ Background Modes
// โœ… Location updates ์ฒดํฌ

// 2. Info.plist์— ์ถ”๊ฐ€
// NSLocationAlwaysAndWhenInUseUsageDescription
// NSLocationAlwaysUsageDescription

// 3. ์ฝ”๋“œ ์„ค์ •
func setupBackgroundLocationUpdates() {
    // Always ๊ถŒํ•œ ์š”์ฒญ (๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜์— ํ•„์ˆ˜)
    locationManager.requestAlwaysAuthorization()
    
    // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์—…๋ฐ์ดํŠธ ํ—ˆ์šฉ
    locationManager.allowsBackgroundLocationUpdates = true
    
    // ์ผ์‹œ ์ •์ง€ ์ž๋™ ํ—ˆ์šฉ (๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ)
    // true: ์‹œ์Šคํ…œ์ด ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ์„ ์œ„ํ•ด ์ž๋™์œผ๋กœ ์ผ์‹œ ์ •์ง€ ๊ฐ€๋Šฅ
    locationManager.pausesLocationUpdatesAutomatically = false
    
    // ์ค‘์š” ์œ„์น˜ ๋ณ€๊ฒฝ ๋ชจ๋‹ˆํ„ฐ๋ง (๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ์ )
    // ์…€ ํƒ€์›Œ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ (์•ฝ 500m~์ˆ˜km ๋‹จ์œ„)
    locationManager.startMonitoringSignificantLocationChanges()
}

// ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ข…๋ฃŒ๋œ ํ›„ ์žฌ์‹คํ–‰๋  ๋•Œ
// AppDelegate์—์„œ ์ฒ˜๋ฆฌ
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•ด ์žฌ์‹คํ–‰๋œ ๊ฒฝ์šฐ
    if let _ = launchOptions?[.location] {
        // LocationManager ์žฌ์ดˆ๊ธฐํ™” ๋ฐ ์—…๋ฐ์ดํŠธ ์žฌ์‹œ์ž‘
        locationManager.startMonitoringSignificantLocationChanges()
    }
    return true
}
๐Ÿ’ก ๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ์ ์ธ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์ „๋žต!

startMonitoringSignificantLocationChanges() โ€” ์…€ ํƒ€์›Œ ๊ธฐ๋ฐ˜, ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ตœ์†Œ. ์•ฝ 500m ์ด์ƒ ์ด๋™ ์‹œ ์—…๋ฐ์ดํŠธ.
startUpdatingLocation() โ€” GPS ๊ธฐ๋ฐ˜, ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ๋†’์Œ. ์‹ค์‹œ๊ฐ„ ์ถ”์  ํ•„์š” ์‹œ ์‚ฌ์šฉ.
์ง€์˜คํŽœ์‹ฑ โ€” ํŠน์ • ์˜์—ญ ์ง„์ž…/์ดํƒˆ๋งŒ ๊ฐ์ง€. ๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ ์ตœ๊ณ .

์•ฑ์˜ ์šฉ๋„์— ๋งž๊ฒŒ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ!

๐Ÿงช 6. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์œ„์น˜ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์‹ค๊ธฐ๊ธฐ ์—†์ด๋„ Xcode ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ ๊ฐœ๋ฐœํ•  ๋•Œ ์—„์ฒญ ํŽธํ•ดใ…‹ใ…‹

๐Ÿ–ฅ๏ธ Xcode ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์œ„์น˜ ์„ค์ • ๋ฐฉ๋ฒ•

๋ฐฉ๋ฒ• 1 โ€” ๋ฉ”๋‰ด์—์„œ ์„ค์ •:
์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์‹คํ–‰ โ†’ Features โ†’ Location โ†’ ์›ํ•˜๋Š” ์œ„์น˜ ์„ ํƒ
(Apple, Apple Store, City Bicycle Ride, City Run, Freeway Drive ๋“ฑ ํ”„๋ฆฌ์…‹ ์ œ๊ณต)

๋ฐฉ๋ฒ• 2 โ€” Xcode Debug ๋ฉ”๋‰ด:
์•ฑ ์‹คํ–‰ ์ค‘ โ†’ Debug โ†’ Simulate Location โ†’ ์œ„์น˜ ์„ ํƒ

๋ฐฉ๋ฒ• 3 โ€” GPX ํŒŒ์ผ๋กœ ๊ฒฝ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜:
์‹ค์ œ ์ด๋™ ๊ฒฝ๋กœ๋ฅผ GPX ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด์„œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์— ๋กœ๋“œํ•˜๋ฉด
์‹ค์ œ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์œ„์น˜๊ฐ€ ๋ณ€ํ•ด!
<!-- GPX ํŒŒ์ผ ์˜ˆ์‹œ (Seoul_Route.gpx) -->
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Xcode">
    <trk>
        <name>์„œ์šธ ํ…Œ์ŠคํŠธ ๊ฒฝ๋กœ</name>
        <trkseg>
            <trkpt lat="37.5665" lon="126.9780">
                <ele>38</ele>
                <time>2024-01-01T09:00:00Z</time>
            </trkpt>
            <trkpt lat="37.5680" lon="126.9800">
                <ele>40</ele>
                <time>2024-01-01T09:00:30Z</time>
            </trkpt>
            <trkpt lat="37.5700" lon="126.9830">
                <ele>42</ele>
                <time>2024-01-01T09:01:00Z</time>
            </trkpt>
        </trkseg>
    </trk>
</gpx>

// GPX ํŒŒ์ผ ์‚ฌ์šฉ๋ฒ•:
// 1. Xcode ํ”„๋กœ์ ํŠธ์— GPX ํŒŒ์ผ ์ถ”๊ฐ€
// 2. Edit Scheme โ†’ Run โ†’ Options โ†’ Default Location โ†’ GPX ํŒŒ์ผ ์„ ํƒ
// 3. ์•ฑ ์‹คํ–‰ ์‹œ GPX ๊ฒฝ๋กœ๋Œ€๋กœ ์œ„์น˜๊ฐ€ ๋ณ€ํ•จ

๐ŸŽฏ 7. SwiftUI์—์„œ MapKit ์‚ฌ์šฉํ•˜๊ธฐ (iOS 17+)

iOS 17๋ถ€ํ„ฐ MapKit์ด SwiftUI์™€ ์™„์ „ํžˆ ํ†ตํ•ฉ๋์–ด! ๊ธฐ์กด UIKit ๋ฐฉ์‹๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ง€๋„๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ดใ…‹ใ…‹

import SwiftUI
import MapKit

// iOS 17+ SwiftUI Map
struct ContentView: View {
    // ์ง€๋„ ์นด๋ฉ”๋ผ ์œ„์น˜ ์ƒํƒœ
    @State private var cameraPosition: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780),
            span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
        )
    )
    
    // ์„ ํƒ๋œ ์žฅ์†Œ
    @State private var selectedPlace: MKMapItem?
    
    // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
    @State private var searchResults: [MKMapItem] = []
    
    var body: some View {
        Map(position: $cameraPosition, selection: $selectedPlace) {
            // ์‚ฌ์šฉ์ž ์œ„์น˜ ํ‘œ์‹œ
            UserAnnotation()
            
            // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋งˆ์ปค ํ‘œ์‹œ
            ForEach(searchResults, id: \.self) { item in
                Marker(item: item)
                    .tint(.blue)
            }
            
            // ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜
            Annotation("์„œ์šธ์‹œ์ฒญ", coordinate: CLLocationCoordinate2D(
                latitude: 37.5665, longitude: 126.9780
            )) {
                ZStack {
                    Circle()
                        .fill(.blue)
                        .frame(width: 40, height: 40)
                    Image(systemName: "building.2")
                        .foregroundColor(.white)
                }
            }
        }
        .mapStyle(.standard(elevation: .realistic))  // 3D ์ง€ํ˜•
        .mapControls {
            MapUserLocationButton()  // ๋‚ด ์œ„์น˜ ๋ฒ„ํŠผ
            MapCompass()             // ๋‚˜์นจ๋ฐ˜
            MapScaleView()           // ์ถ•์ฒ™
        }
        .onChange(of: selectedPlace) { _, newValue in
            if let place = newValue {
                print("์„ ํƒ๋œ ์žฅ์†Œ: \(place.name ?? "")")
            }
        }
    }
}

// iOS 17+ ์œ„์น˜ ๊ด€๋ฆฌ (Observable)
@Observable
class LocationService: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    var currentLocation: CLLocation?
    var authorizationStatus: CLAuthorizationStatus = .notDetermined
    
    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
    }
    
    func requestPermission() {
        manager.requestWhenInUseAuthorization()
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
        if authorizationStatus == .authorizedWhenInUse {
            manager.startUpdatingLocation()
        }
    }
    
    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        currentLocation = locations.last
    }
}
๐Ÿ’ก SwiftUI Map์˜ ์žฅ์ !
iOS 17+์—์„œ SwiftUI Map์€ ์„ ์–ธํ˜• ๋ฌธ๋ฒ•์œผ๋กœ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ง€๋„๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ @Observable ๋งคํฌ๋กœ์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ์œ„์น˜ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์—„์ฒญ ๊น”๋”ํ•ด์ ธ. ์‹ ๊ทœ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด SwiftUI Map์„ ์ ๊ทน ํ™œ์šฉํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ด!

๐Ÿ›ก๏ธ 8. ํ”„๋ผ์ด๋ฒ„์‹œ์™€ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” โ€” ์ง„์งœ ์ค‘์š”ํ•œ ๋ถ€๋ถ„!

์œ„์น˜ ์„œ๋น„์Šค ๊ฐœ๋ฐœ์—์„œ ๊ธฐ์ˆ ์ ์ธ ๊ตฌํ˜„๋งŒํผ์ด๋‚˜ ์ค‘์š”ํ•œ ๊ฒŒ ํ”„๋ผ์ด๋ฒ„์‹œ ๋ณดํ˜ธ์™€ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”์•ผ. ์ด๊ฑธ ์†Œํ™€ํžˆ ํ•˜๋ฉด ์•ฑ์Šคํ† ์–ด ๋ฆฌ์ ์€ ๋ฌผ๋ก ์ด๊ณ  ์‚ฌ์šฉ์ž ์‹ ๋ขฐ๋„ ์žƒ์–ดใ…‹ใ…‹

8-1. ํ”„๋ผ์ด๋ฒ„์‹œ ์ตœ์„  ๊ด€ํ–‰

๐Ÿ”’ ์œ„์น˜ ํ”„๋ผ์ด๋ฒ„์‹œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ โ€” Always ๊ถŒํ•œ์ด ํ•„์š” ์—†์œผ๋ฉด When In Use๋งŒ ์š”์ฒญ
  • โœ… ์‚ฌ์šฉ ๋ชฉ์  ๋ช…ํ™•ํžˆ ๊ณ ์ง€ โ€” Info.plist ์„ค๋ช… ๋ฌธ์ž์—ด์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑ
  • โœ… ์œ„์น˜ ๋ฐ์ดํ„ฐ ์ตœ์†Œ ์ˆ˜์ง‘ โ€” ํ•„์š”ํ•œ ์ •ํ™•๋„๋งŒ ์‚ฌ์šฉ
  • โœ… ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™” ์ €์žฅ โ€” ์œ„์น˜ ์ด๋ ฅ์„ ๋กœ์ปฌ์— ์ €์žฅํ•  ๋•Œ ์•”ํ˜ธํ™”
  • โœ… ์„œ๋ฒ„ ์ „์†ก ์ตœ์†Œํ™” โ€” ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์„œ๋ฒ„๋กœ ์œ„์น˜ ์ „์†ก
  • โœ… ์‚ฌ์šฉ์ž ์ œ์–ด๊ถŒ ์ œ๊ณต โ€” ์•ฑ ๋‚ด์—์„œ ์œ„์น˜ ์ถ”์  on/off ๊ธฐ๋Šฅ
  • โœ… ๋ฐ์ดํ„ฐ ๋ณด์กด ๊ธฐ๊ฐ„ ๋ช…์‹œ โ€” ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์–ผ๋งˆ๋‚˜ ๋ณด๊ด€ํ•˜๋Š”์ง€ ๊ณ ์ง€
  • โœ… ๋ถˆํ•„์š” ์‹œ ์ฆ‰์‹œ ์ค‘์ง€ โ€” ์œ„์น˜๊ฐ€ ํ•„์š” ์—†์„ ๋•Œ stopUpdatingLocation() ํ˜ธ์ถœ

8-2. ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” ์ „๋žต

// ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์ ์‘ํ˜• ์œ„์น˜ ์—…๋ฐ์ดํŠธ
class AdaptiveLocationManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    private var isMoving = false
    
    // ์ด๋™ ์ค‘์ผ ๋•Œ์™€ ์ •์ง€ ์ค‘์ผ ๋•Œ ๋‹ค๋ฅธ ์„ค์ • ์ ์šฉ
    func updateAccuracyBasedOnMovement(speed: Double) {
        if speed > 1.0 {  // 1m/s ์ด์ƒ = ์ด๋™ ์ค‘
            if !isMoving {
                isMoving = true
                // ์ด๋™ ์ค‘: ๋†’์€ ์ •ํ™•๋„
                manager.desiredAccuracy = kCLLocationAccuracyBest
                manager.distanceFilter = 5  // 5m๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ
            }
        } else {  // ์ •์ง€ ์ค‘
            if isMoving {
                isMoving = false
                // ์ •์ง€ ์ค‘: ๋‚ฎ์€ ์ •ํ™•๋„๋กœ ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ
                manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
                manager.distanceFilter = 50  // 50m๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        updateAccuracyBasedOnMovement(speed: location.speed)
    }
    
    // ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐˆ ๋•Œ ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ ๋ชจ๋“œ
    func applicationDidEnterBackground() {
        manager.desiredAccuracy = kCLLocationAccuracyKilometer
        manager.distanceFilter = 500
    }
    
    // ์•ฑ์ด ํฌ๊ทธ๋ผ์šด๋“œ๋กœ ์˜ฌ ๋•Œ ์ •ํ™•๋„ ๋ณต์›
    func applicationWillEnterForeground() {
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.distanceFilter = 10
    }
}

์ฐธ๊ณ ๋กœ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ๋„ "๋‚ด ๊ทผ์ฒ˜ ์žฌ๋Šฅ์ธ ์ฐพ๊ธฐ" ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์ด๋Ÿฐ ์œ„์น˜ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์ด ํ•„์ˆ˜์•ผ! ์‚ฌ์šฉ์ž ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ์•„๋ผ๋ฉด์„œ๋„ ์ •ํ™•ํ•œ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋งค์นญ์„ ์ œ๊ณตํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด๊ฑฐ๋“  ๐Ÿ˜„


๐Ÿ”ง 9. ์ž์ฃผ ๋งŒ๋‚˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ๋ฒ•

9-1. ์œ„์น˜๊ฐ€ ๊ฐ‘์ž๊ธฐ ํŠ€๋Š” ๋ฌธ์ œ (Location Spike)

// ์œ„์น˜ ํА ํ˜„์ƒ ํ•„ํ„ฐ๋ง
func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }
    
    // 1. ์ •ํ™•๋„ ํ•„ํ„ฐ - ๋ถ€์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ ๋ฌด์‹œ
    guard location.horizontalAccuracy >= 0,
          location.horizontalAccuracy <= 65 else { return }
    
    // 2. ํƒ€์ž„์Šคํƒฌํ”„ ํ•„ํ„ฐ - ์˜ค๋ž˜๋œ ์บ์‹œ ๋ฐ์ดํ„ฐ ๋ฌด์‹œ
    let age = -location.timestamp.timeIntervalSinceNow
    guard age < 5 else { return }  // 5์ดˆ ์ด๋‚ด ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉ
    
    // 3. ์†๋„ ํ•„ํ„ฐ - ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅํ•œ ์†๋„ ๋ฌด์‹œ
    if let previousLocation = previousLocation {
        let distance = location.distance(from: previousLocation)
        let timeInterval = location.timestamp.timeIntervalSince(previousLocation.timestamp)
        let calculatedSpeed = distance / timeInterval
        
        // ์‹œ์† 300km ์ด์ƒ์ด๋ฉด ํА ํ˜„์ƒ์œผ๋กœ ํŒ๋‹จ
        guard calculatedSpeed < 83.3 else { return }  // 300km/h = 83.3m/s
    }
    
    previousLocation = location
    // ์œ ํšจํ•œ ์œ„์น˜ ์ฒ˜๋ฆฌ
    processValidLocation(location)
}

9-2. ์‹ค๋‚ด์—์„œ ์œ„์น˜ ์ •ํ™•๋„ ์ €ํ•˜

๐Ÿ’ก ์‹ค๋‚ด ์œ„์น˜ ์ •ํ™•๋„ ๊ฐœ์„  ํŒ!
์‹ค๋‚ด์—์„œ๋Š” GPS ์‹ ํ˜ธ๊ฐ€ ์•ฝํ•ด์„œ Wi-Fi์™€ ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ด. ์ด๋•Œ horizontalAccuracy๊ฐ€ ์ˆ˜์‹ญ~์ˆ˜๋ฐฑ ๋ฏธํ„ฐ๋กœ ์ปค์งˆ ์ˆ˜ ์žˆ์–ด.

ํ•ด๊ฒฐ์ฑ…:
1. horizontalAccuracy๋ฅผ UI์— ํ‘œ์‹œํ•ด์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ ค์ฃผ๊ธฐ
2. ์ •ํ™•๋„๊ฐ€ ๋‚ฎ์„ ๋•Œ๋Š” ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ฑฐ๋‚˜ ๋งˆ์ง€๋ง‰ ์œ ํšจ ์œ„์น˜ ์‚ฌ์šฉ
3. iBeacon(๋ธ”๋ฃจํˆฌ์Šค ๋น„์ฝ˜)์„ ํ™œ์šฉํ•œ ์‹ค๋‚ด ์ธก์œ„ ๊ตฌํ˜„ ๊ณ ๋ ค
4. Core Motion๊ณผ ๊ฒฐํ•ฉํ•ด์„œ ๋ณดํ–‰์ž ์ถ”์ธก ํ•ญ๋ฒ•(PDR) ํ™œ์šฉ

9-3. ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ์‚ฌ์šฉ์ž ์•ˆ๋‚ด

// ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ์„ค์ • ์•ฑ์œผ๋กœ ์œ ๋„
func showLocationPermissionAlert() {
    let alert = UIAlertController(
        title: "์œ„์น˜ ๊ถŒํ•œ ํ•„์š”",
        message: "์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์œ„์น˜ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ค์ •์—์„œ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”.",
        preferredStyle: .alert
    )
    
    alert.addAction(UIAlertAction(title: "์ทจ์†Œ", style: .cancel))
    alert.addAction(UIAlertAction(title: "์„ค์ •์œผ๋กœ ์ด๋™", style: .default) { _ in
        // ์•ฑ ์„ค์ • ํ™”๋ฉด์œผ๋กœ ์ด๋™
        if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.open(settingsURL)
        }
    })
    
    present(alert, animated: true)
}

// ๊ถŒํ•œ ์ƒํƒœ ํ™•์ธ ํ›„ ์ ์ ˆํ•œ ์ฒ˜๋ฆฌ
func checkAndRequestPermission() {
    switch locationManager.authorizationStatus {
    case .notDetermined:
        locationManager.requestWhenInUseAuthorization()
    case .denied, .restricted:
        showLocationPermissionAlert()
    case .authorizedWhenInUse, .authorizedAlways:
        locationManager.startUpdatingLocation()
    @unknown default:
        break
    }
}

๐Ÿ“Š 10. Core Location vs MapKit ํ•ต์‹ฌ ์ •๋ฆฌ

๊ตฌ๋ถ„ Core Location MapKit
์—ญํ•  ์œ„์น˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘/์ฒ˜๋ฆฌ ์ง€๋„ ์‹œ๊ฐํ™”/UI
ํ•ต์‹ฌ ํด๋ž˜์Šค CLLocationManager, CLLocation MKMapView, MKAnnotation
๊ถŒํ•œ ํ•„์š” โœ… ํ•„์ˆ˜ โŒ ๋ถˆํ•„์š” (์ง€๋„ ํ‘œ์‹œ๋งŒ)
๋ฐฐํ„ฐ๋ฆฌ ์˜ํ–ฅ ๋†’์Œ (GPS ์‚ฌ์šฉ) ๋‚ฎ์Œ (๋ Œ๋”๋ง๋งŒ)
์ฃผ์š” ๊ธฐ๋Šฅ ์œ„์น˜ ์ถ”์ , ์ง€์˜คํŽœ์‹ฑ, ์ง€์˜ค์ฝ”๋”ฉ ์ง€๋„ ํ‘œ์‹œ, ์–ด๋…ธํ…Œ์ด์…˜, ๊ฒฝ๋กœ
๋…๋ฆฝ ์‚ฌ์šฉ โœ… ๊ฐ€๋Šฅ (์ง€๋„ ์—†์ด ์œ„์น˜๋งŒ) โœ… ๊ฐ€๋Šฅ (์œ„์น˜ ์—†์ด ์ง€๋„๋งŒ)
SwiftUI ์ง€์› @Observable๋กœ ํ†ตํ•ฉ Map() ๋ทฐ๋กœ ์™„์ „ ์ง€์›
๐ŸŽฏ ์ตœ์ข… ์ •๋ฆฌ!
LBS ์•ฑ ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ์€ ๊ธฐ์ˆ ์  ๊ตฌํ˜„๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ๊ท ํ˜•์ด์•ผ.

โ‘  ๊ถŒํ•œ์€ ํ•„์š”ํ•œ ์ตœ์†Œํ•œ๋งŒ ์š”์ฒญ
โ‘ก ์ •ํ™•๋„๋Š” ์•ฑ ์šฉ๋„์— ๋งž๊ฒŒ ์„ ํƒ
โ‘ข ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ๋ฅผ ํ•ญ์ƒ ๊ณ ๋ ค
โ‘ฃ ์œ„์น˜ ๋ฐ์ดํ„ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ฒ ์ €ํžˆ
โ‘ค ํ”„๋ผ์ด๋ฒ„์‹œ ์ •์ฑ… ๋ช…ํ™•ํžˆ ๊ณ ์ง€

์ด ๋‹ค์„ฏ ๊ฐ€์ง€๋งŒ ์ž˜ ์ง€์ผœ๋„ ํ›Œ๋ฅญํ•œ LBS ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿš€
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.