ABOUT ME

Today
Yesterday
Total
  • [iOS/SwiftUI] Kakao Local API - 키워드로 내 주변 장소 탐색
    Apple🍎/iOS 2024. 4. 2. 16:18

    Kakao local API를 사용하게 된 이유

    EYE-Mate 프로젝트에서 사용하는 내주변 안과, 안경점 탐색을 담당하던 Naver Search Place API 쿼리가 서비스를 중단하면서.. 어떻게 할까 고민하다가 Kakao Local API가 주변 장소 탐색을 지원하고 한번에 15개의 장소를 반환할 수 있다는 것에 카카오 API를 선택했다.

    원래 사용하던 naver api json 구조에는 장소의 기본 정보 뿐 아니라 오픈 상태, 썸네일 등등이 추가적으로 주어졌는데, kakao & naver둘다 API로 제공하는 장소 정보를 줄여버렸다.

    kakao 지도보다는 네이버 지도가 더 친숙하고 이미 구현해놨기도 해서 카카오 api로 데이터만 받아 장소 위치좌표를 네이버 지도에 뿌리려한다.

    API를 받아오는 과정은

    1. API 응답을 위한 JSON 자료형 구조체를 만들어준다.
    2. URLsession GET 통신으로 받아온 것들은 전부 string이므로, 이것이 json 구조체임을 알기 위해 위에서 만든 구조체로 해석한다.
    3. 이제 dictionary로 만들어 원하는 데이터를 ui에 넣어주면된다.

     

    JSON 자료 구조체를 만드려면, 우선 API 응답을 받아서 형태를 파악해야한다.

    [카카오 REST API]https://developers.kakao.com/docs/latest/ko/local/dev-guide#see-more

    • 아래와 같은 명령어를 terminal이나 API 통신하는 툴에 넣어 JSON을 확인한다.
    curl -v -G GET "<https://dapi.kakao.com/v2/local/search/keyword.json?y=37.628187&x=127.082911&radius=20000&query=안경점&category_group_code=&sort=distance&page=1>"\\
      -H "Authorization: KakaoAK ${Kakao REST_API_KEY}"

    응답 받은 JSON

    {
    "documents":[
    {"address_name":"서울 강남구 삼성동 154-11","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강 병원 안과","distance":"672","id":"10471957","phone":"02-552-0055","place_name":"세란안과의원","place_url":"<http://place.map.kakao.com/10471957","road_address_name":"서울> 강남구 테헤란로87길 29","x":"127.05795459428911","y":"37.50967047808699"},
    {"address_name":"서울 강남구 대치동 945-5","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"691","id":"1702177560","phone":"02-2051-0500","place_name":"리뉴서울안과의원","place_url":"<http://place.map.kakao.com/1702177560","road_address_name":"서울> 강남구 테헤란로 528","x":"127.06190970707586","y":"37.50813045646588"},
    {"address_name":"서울 강남구 대치동 889-11","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"1551","id":"1621132696","phone":"1661-1175","place_name":"누네안과병원 서울병원","place_url":"<http://place.map.kakao.com/1621132696","road_address_name":"서울> 강남구 테헤란로 408","x":"127.05038442566595","y":"37.50446614104791"},
    {"address_name":"서울 강남구 대치동 944","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"811","id":"27532609","phone":"02-555-3700","place_name":"예드림안과","place_url":"<http://place.map.kakao.com/27532609","road_address_name":"서울> 강남구 테헤란로 514","x":"127.05974551269134","y":"37.507433292186434"},
    {"address_name":"서울 강남구 삼성동 117","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"978","id":"1971390486","phone":"02-6953-5826","place_name":"솔빛드림안과의원","place_url":"<http://place.map.kakao.com/1971390486","road_address_name":"서울> 강남구 봉은사로 474","x":"127.052034340659","y":"37.5123816534486"},
    {"address_name":"서울 강남구 삼성동 19-4","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"1216","id":"16616667","phone":"02-546-5975","place_name":"YK안과의원","place_url":"<http://place.map.kakao.com/16616667","road_address_name":"서울> 강남구 삼성로 651","x":"127.05003857139373","y":"37.51835076419007"},
    {"address_name":"서울 강남구 청담동 46-19","category_group_code":"HP8","category_group_name":"병원","category_name":"의료,건강  병원  안과","distance":"1374","id":"193195929","phone":"02-544-1645","place_name":"청담한세성모안과","place_url":"<http://place.map.kakao.com/193195929","road_address_name":"서울> 강남구 학동로 433","x":"127.048314929125","y":"37.5187623392971"}....

     

    위의 구조를 복사하여 아래의 사이트에 넣으면 swift 구조체를 만들어준다.

    Instantly parse JSON in any language | quicktype

     

    Instantly parse JSON in any language | quicktype

     

    app.quicktype.io

    위에서 만든 후에도 계속 아래의 코드로 GET 통신을 하며, 잘 불러와지는지 확인 해야한다.

    왜냐하면 위처럼 JSON에는 없는 정보가 있을 수도 있고, 여기서 만들어준 구조체를 너무 믿으면 안된다.

    위 사이트 사용 Tip

    • Plain types only, Explicit CodingKey values~~ 만 체크해서 본다
    • 그래야 깔끔한 구조체가 나오는 것 같아서, 이후 받은 다음에 수정하는 편이다.
    • 구조체로 Decoding이 잘 안될 때 해결법 
      • 가끔 읽을 수 없는 경우 Codable 프로토콜을 넣어주면 데이터간의 배열로 인식하기에 좋다.
      • 위처럼 키값에 “_”가 포함되어 있는 경우, 아래처럼 CodingKeys 설정을 해주면 해결되는 경우가 있다.
      •  
    enum CodingKeys: String, CodingKey {
            case addressName = "address_name"
            case categoryGroupCode = "category_group_code"
            case categoryGroupName = "category_group_name"
            case categoryName = "category_name"
            case distance, id, phone
            case placeName = "place_name"
            case placeURL = "place_url"
            case roadAddressName = "road_address_name"
            case x, y
        }

     

    구조체 전체 (Places.swift)

    import Foundation
    
    // MARK: - Places
    struct Places: Codable {
        let documents: [PlaceList]
    }
    
    // MARK: - Document
    struct PlaceList: Codable {
        let addressName: String
        let categoryGroupCode: String
        let categoryGroupName: String
        let categoryName: String
        let distance, id, phone, placeName: String
        let placeURL: String
        let roadAddressName, x, y: String
        
        enum CodingKeys: String, CodingKey {
            case addressName = "address_name"
            case categoryGroupCode = "category_group_code"
            case categoryGroupName = "category_group_name"
            case categoryName = "category_name"
            case distance, id, phone
            case placeName = "place_name"
            case placeURL = "place_url"
            case roadAddressName = "road_address_name"
            case x, y
        }
    }

     

    JSON 디코딩 함수 구현

    func fetchApiData() {
            // Kakao API 사용, y(latitude) = 37, x(longitude) = 127
            // 기본 15개 제공
            guard let url = URL(string: "<https://dapi.kakao.com/v2/local/search/keyword.json?y=\\(String(coord.0)>)&x=\\(String(coord.1))&radius=20000&query=\\(queryPlace)&category_group_code=\\(placeCode)&sort=distance&page=1")
            else {
                print("Invalid URL")
                return
            }
            
            // Request
            var request = URLRequest(url: url)
            request.setValue(Bundle.main.kakaoAPIKey, forHTTPHeaderField: "Authorization")
            let session = URLSession(configuration: .default)
            
            // Task
            session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
                guard error == nil else {
                    print("Error occur: error calling GET - \\(String(describing: error))")
                    return
                }
                guard let data = data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
                    print("Error: HTTP request failed", response as Any)
                    return
                }
                guard let output = try? JSONDecoder().decode(Places.self, from: data) else {
                    print("Error: JSON data parsing failed")
                    return
                }
                
                DispatchQueue.main.async {
                    // DATA 보여주기 
                    var array = output.documents
                    // 내 주변 15개 보여주기
                    let resultArray = Array(array)
                    
                    // 위치 기준 새로운 마커 생성
                    var markNumber: Int = -1 // 마커 식별자 테그
                    var placeIdx: Int = 0
                    resultArray.forEach { element in
                        markNumber += 1
                        self.hospitals.append((Double(element.y) ?? 0.0, Double(element.x) ?? 0.0))
                        
                        let marker = NMFMarker()
                        marker.position = NMGLatLng(lat: Double(element.y) ?? 0.0, lng: Double(element.x) ?? 0.0)
                        marker.iconImage = NMFOverlayImage(name: self.markerImage)
                        marker.width = 50
                        marker.height = 50
                        marker.mapView = self.view.mapView
                        marker.touchHandler = { [self] (overlay: NMFOverlay) -> Bool in
                            // 눌렀을때 정보 확인용 (디버깅)
                            print(resultArray[Int(marker.tag)])
                            placeIdx = Int(marker.tag)
                            self.placeInfo[Key.name.rawValue] = resultArray[placeIdx].placeName
                            self.placeInfo[Key.address.rawValue] = resultArray[placeIdx].addressName
                            self.placeInfo[Key.roadAddress.rawValue] = resultArray[placeIdx].roadAddressName
                            // 사용자 위치 기준 거리로 변환
                            self.placeInfo[Key.distance.rawValue] = self.getMeter(lat1: userLocation.0, lon1: userLocation.1, lat2: Double(resultArray[placeIdx].y) ?? 1.0, lon2: Double(resultArray[placeIdx].x) ?? 1.0).convertMeter()
                            self.placeInfo[Key.tel.rawValue] = resultArray[placeIdx].phone
                            self.placeInfo[Key.lat.rawValue] = resultArray[placeIdx].y
                            self.placeInfo[Key.lng.rawValue] = resultArray[placeIdx].x
                            withAnimation(.linear(duration: 0.25)) {
                                self.sheetFlag = true
                            }
                            
                            return true // 이벤트 소비, -mapView:didTapMap:point 이벤트는 발생하지 않음
                        }
                        marker.tag = UInt(markNumber)
                        self.hospitalsMarkers.append(marker)
                    }
                }
            }.resume()
        }

     

    가져온 데이터 입맛대로 변형하기

    카카오에서 제공하는 걸로 distance 값을 제공하긴 하지만,

    내가 구현하는 앱에선 지도를 움직이면 그 가운데 지도 좌표의 근처 장소들을 보여준다.

    그렇기 때문에 distance값이 지도 좌표 가운데를 기준으로 m를 보여주는데,,

    사용자 입장에선 m, km 표시를 보면 자기 위치 기준으로 생각하기 때문에

    현재 사용자의 좌표, 장소 좌표를 기준으로 거리를 구하여 m를 제공했다.

    아래 함수를 통해서 제공

    현재 좌표, 장소 좌표를 기준으로 거리 구하기

    func getMeter(lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> String {
            let deltaLon = abs(deg2rad(lon2) - deg2rad(lon1))
            let deltaLat = abs(deg2rad(lat2) - deg2rad(lat1))
            
            let distance = acos(sin(deg2rad(lat1)) * sin(deg2rad(lat2)) + cos(deg2rad(lat1)) * cos(deg2rad(lat2)) * cos(deltaLon)) * 3963.189 // 마일
            
            let gap = Int(distance * 1609.344)
            return String(gap)
        }
    
    func deg2rad(_ val: Double) -> Double {
        let pi = Double.pi
        let deRa = val * (pi / 180.0)
        return deRa
    }

     

    완성 화면

    똑같이 구현했는데 캡처를 안해서 ㅎ.. figma 에서 가져왔기때문에 실제 데이터는 안맞아요 감안해서 봐주세요 :)

    'Apple🍎 > iOS' 카테고리의 다른 글

    [iOS/OS] 메모리 구조  (2) 2024.08.23
    [iOS/UIKit] UICollectionViewLayout 정리  (1) 2024.05.17
    [iOS] CoreData vs UserDefaults  (0) 2024.04.15
    [iOS] Cocoapods, Carthage, Swift Package Manager  (0) 2024.04.15
    [iOS] Xcode Playground  (0) 2023.09.27
Designed by Tistory.