프로젝트

Flutter web 구글 directions API로 도착 소요 시간 가져오기

TMJeti 2025. 4. 24. 12:15

이제 FastAPI 서버 연동해봤으니 거기서 API key를 가져올 수 있는지 확인해야한다. (연동 방법은 이전 포스트 참조)

 

env 파일에서 key 잘 가져왔다! 이제 드디어 정말로 최적 경로와 시간을 가져와 볼것이다.

 

우선 main 부분부터 바꿔준다.

# main.py

from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
import requests
import os
from dotenv import load_dotenv

# .env 불러오기
load_dotenv()

app = FastAPI()

# CORS 설정 (Flutter Web용)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # dev 용도, 배포 시엔 특정 origin만
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")

@app.get("/directions")
def get_directions(
    origin: str = Query(...),
    destination: str = Query(...),
    mode: str = Query("driving")
):
    url = "https://maps.googleapis.com/maps/api/directions/json"
    params = {
        "origin": origin,
        "destination": destination,
        "mode": mode,
        "key": GOOGLE_API_KEY
    }

    response = requests.get(url, params=params)
    return response.json()

저 "driving" 이란 값은 mode 설정하지 않으면 들어가는 기본 값으로, 호출할 때 mode = walking 이런식으로 넣어주면 도보 정보 또한 가져올 수 있다. 

 

Flutter project로 넘어가서 uri 호출부 가본다.

기존에 불러오는 코드는 이런 식이라 CORS 정책에 걸리지만

final carUri = Uri.parse('https://maps.googleapis.com/maps/api/directions/json?...');
final res = await http.get(carUri); // CORS 보안문제로 걸린다.

 

아래와 같이 짜면 FastAPI 서버로 넘어간 후 그 서버에서 Directions API를 호출하게 된다. 

// car
final carUri = Uri.parse('http://localhost:8000/directions').replace(queryParameters: {
  'origin': '$startLat,$startLng',
  'destination': '$endLat,$endLng',
  'mode': 'driving',
});
final res = await http.get(carUri); // ✅ FastAPI가 대신 Google API 호출
// 도보
final walkUri = Uri.parse('http://localhost:8000/directions').replace(queryParameters: {
  'origin': '$startLat,$startLng',
  'destination': '$endLat,$endLng',
  'mode': 'walking',
});
final walkRes = await http.get(walkUri);

  • ModuleNotFoundError: No module named 'requests'

이런 오류가 떴는데 requests 모듈 없다는 뜻이다.

가상 환경에서 pip install requests 치고 모듈 다운받아주면 된다.

  • 네트워크 오류 : RangeError(index) : Index out of range : no indices are valid: 0

final carDuration = (jsonDecode(carRes.body)['routes'][0]['legs'][0]['duration']['text']) ?? '정보 없음';

이 줄에서 'routes' 가 비어있어서 생기는 문제이다. 왜 비어있는지 원인 파악을 위해 debugPrint 한번 써보겠다.

if (carRes.statusCode == 200 && walkRes.statusCode == 200) {
          debugPrint("길찾기 API 응답 성공");
          final carData = json.decode(carRes.body);
          final walkData = json.decode(walkRes.body);
          debugPrint("자동차 응답 내용: ${jsonEncode(carData)}");
          debugPrint("도보 응답 내용: ${jsonEncode(walkData)}");

 

길찾기 API 응답 성공
자동차 응답 내용:
{"available_travel_modes":["TRANSIT"],"geocoded_waypoints":[{},{}],"r
outes":[],"status":"ZERO_RESULTS"}
도보 응답 내용:
{"available_travel_modes":["TRANSIT"],"geocoded_waypoints":[{},{}],"r
outes":[],"status":"ZERO_RESULTS"}

 

ZERO_RESULTS가 뜬다. Google Directions API는 특정 구간이 너무 멀거나, 도보나 자동차 경로가 실제로 없다고 판단되면 빈 routes를 돌려준다고 한다(...) 실제 좌표가 이상한 건가 싶어서 들어가는 좌표 확인해보고 오겠다.

// 좌표 가져오기 (lat/lng 형태로 저장되어 있는 경우)
      double? startLat = start['lat'] is double ? start['lat'] : double.tryParse(start['lat']?.toString() ?? '');
      double? startLng = start['lng'] is double ? start['lng'] : double.tryParse(start['lng']?.toString() ?? '');
      double? endLat = end['lat'] is double ? end['lat'] : double.tryParse(end['lat']?.toString() ?? '');
      double? endLng = end['lng'] is double ? end['lng'] : double.tryParse(end['lng']?.toString() ?? '');
      debugPrint("장소 좌표: ${start['name']}(${startLat}, ${startLng}) → ${end['name']}(${endLat}, ${endLng})");

 

시작 좌표와 끝 좌표에 debug 찍어줬다. 결과 확인해보면

🧭 좌표 디버깅: 씨네 인디 유(36.323769, 127.426711) → 왕관식당(36.315315, 127.437365) 🔍 길찾기 시작: 씨네 인디 유 → 왕관식당

 

잘나오는데??

 Aㅏ... 그렇다.. 구글맵에 실제 경로 검색해보니 애초에 Google API는 대중교통 경로만 있고 자동차, 도보 경로는 존재 자체가 없던 것이었다...


대중교통 만이라도 가져와 보겠다...

final transitUri = Uri.parse('http://localhost:8000/directions').replace(queryParameters: {
  'origin': '$startLat,$startLng',
  'destination': '$endLat,$endLng',
  'mode': 'transit',
});

        debugPrint(" 대중교통 API 요청: ${transitUri.toString()}");
        final transitRes = await http.get(transitUri); // FastAPI가 대신 Google API 호출
        debugPrint("대중교통통 API 응답 코드: ${transitRes.statusCode}");
final carDuration = (jsonDecode(carRes.body)['routes'][0]['legs'][0]['duration']['text']) ?? '정보 없음';
          final walkDuration = (jsonDecode(walkRes.body)['routes'][0]['legs'][0]['duration']['text']) ?? '정보 없음';
          # 대중교통 Duration 추가
          final transitDuration = (jsonDecode(transitRes.body)['routes'][0]['legs'][0]['duration']['text']) ?? '정보 없음';

          times.add({
            'car': carDuration,
            'walk': walkDuration,
            'transit': transitDuration,
        });

 

# UI 부분

if (index < travelTimes.length) 
                            Container(
                              margin: EdgeInsets.only(left: 16, bottom: 16),
                              padding: EdgeInsets.all(12),
                              decoration: BoxDecoration(
                                color: Colors.grey[100],
                                borderRadius: BorderRadius.circular(8),
                              ),
                              child: Column(
                                children: [
                                  Row(
                                    children: [
                                      Icon(Icons.directions_car, size: 16),
                                      SizedBox(width: 4),
                                      Text('자동차: ${travelTimes[index]['car']}'),
                                    ],
                                  ),
                                  SizedBox(height: 4),
                                  Row(
                                    children: [
                                      Icon(Icons.directions_walk, size: 16),
                                      SizedBox(width: 4),
                                      Text('도보: ${travelTimes[index]['walk']}'),
                                    ],
                                  ),
                                  Row(
                                    children: [
                                      Icon(Icons.directions_transit, size: 16),
                                      SizedBox(width: 4),
                                      Text('대중교통: ${travelTimes[index]['transit']}'),
                                    ],
                                  ),
                                ],
                              ),
                            ),

 

🚍 대중교통 응답 내용:
{"geocoded_waypoints":[{"geocoder_status":"OK","place_id":"ChIJa8pE0cVLZTURvRDhysQtuRM","types":["street_address"]},{"geocoder_status":"OK","place_id":"ChIJgab-LhhMZ
TURksQhCs0607M","types":["street_address"]}],"routes":[{"bounds":{"northeast":{"lat":36.36489,"lng":127.379312},"southwest":{"lat":36.31909,"lng":127.367342}},"copyrights":"Powered by Google, Â©2025

 

잘들어온다... 하핳

젠장 잘 들어온다 결과가 잘나오는데 앞서 댕고생 했던 시간을 생각하니 짜증이 치솟는다

... 아무래도 자동차, 도보 정보까지 가져오려면 naver directions API와 혼합해서 사용해야 할 필요가 있겠다. 다음 포스트에서 진행하겠다,,