본문 바로가기

프로젝트 (Travel Maker)

Tmap API로 목적지 도착 예상 소요 시간 가져오기 (도보)

최종코드

 

SK open API

 

SK open API

SK텔레콤 데이터와 시각화 가공을 지원 받을 수 있는, 데이터 바우처 사업 에 대해서 알아보세요! 더 알아보기

openapi.sk.com

 

여기에서 회원가입 후 대시보드로 들어가준다.

 

(이미 만들어 놓았다) 이다음 앱 오른쪽 화살표 눌러서

앱 만들기에서 이름만 입력해주면 된다.

Tmap을 사용하려면 따로 신청을 해줘야 하는데 홈화면에 > Products > TMAP > API 사용 요금 창으로 가서 원하는 요금제 사용하기 버튼 누르면 된다. 필자는 Free로 했고, 자동 신청 등록되어 바로 쓸 수 있다. 

 

왼쪽 짝대기 세개 누르면 나온다. 

 

먼저 env 파일에 앱키를 넣어준다음 fastAPI 서버 호출부에 tmap 호출을 추가한다.

# 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=["*"],  # 개발 단계에서만 *, 배포 시엔 도메인 지정 권장
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 환경변수에서 API 키 불러오기
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
TMAP_API_KEY = os.getenv("TMAP_API_KEY")

# 기본 루트 엔드포인트
@app.get("/")
def read_root():
    return {
        "message": "FastAPI is running!",
        "google_key_loaded": bool(GOOGLE_API_KEY),
        "tmap_key_loaded": bool(TMAP_API_KEY),
    }

# ✅ Google Directions API (driving / walking / transit)
@app.get("/directions")
def get_directions(
    origin: str = Query(...),
    destination: str = Query(...),
    mode: str = Query("driving")  # driving, walking, transit
):
    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()

# ✅ Tmap 도보 경로 요청 API
@app.get("/tmap/walk")
def get_tmap_walk_time(
    start_lat: float = Query(...),
    start_lng: float = Query(...),
    end_lat: float = Query(...),
    end_lng: float = Query(...)
):
    url = "https://apis.openapi.sk.com/tmap/routes/pedestrian"
    headers = {
        "Content-Type": "application/json",
        "appKey": TMAP_API_KEY
    }
    body = {
        "startX": str(start_lng),
        "startY": str(start_lat),
        "endX": str(end_lng),
        "endY": str(end_lat),
        "reqCoordType": "WGS84GEO",
        "resCoordType": "WGS84GEO"
    }

    response = requests.post(url, json=body, headers=headers)
    return response.json()

 

서버 정상 작동 확인 되었고, 이제 flutter 단에서 정보를 불러와보겠다.


❌ 도보 API 응답 실패: {"detail":[{"type":"missing","loc":["query","start_lat"],"msg":"Field required","input":null},{"type":"missing","loc":["query","start_lng"] ,"msg":"Field required","input":null},{"type":"missing","loc":["query","end_lat"]," msg":"Field required","input":null},{"type":"missing","loc":["query","end_lng"]," msg":"Field required","input":null}]}

 

이런 오류가 떴는데 

자세히 보면 서버는 start_lat 이런식으로 _ 를 쓴 이름인 반면에 flutter 쪽에서는 startLat 이런 이름이다. 이름이 다르면  못가져오는게 당연하겠지 (...) 다시 서버 이름으로 제대로 맞춰주면

 

✅ 도보 API 응답 성공 (Tmap)
Tmap 도보 응답 원본:
{"error":{"id":"400","category":"tmap","code":"9401","message":"íì   
 íë¼ë©í°ê° ììµëë¤."}}
🚶 Tmap 응답에 features 없음

 

//  안됐다...

할 수 없이 sk open API 사이트에서 오류코드를 찾아보니 필수 파라미터가 없다고 한다.

 

계속 안돼서 서버에 직접 파라미터 넣어서 요청해봤다.

http://localhost:8000/tmap/walk?startX=127.384547&startY=36.351411&endX=127.388727&endY=36.350528
{"error":{"id":"400","category":"tmap","code":"9401","message":"필수 파라메터가 없습니다."}}

역시 안된다. 파라미터 내용이 뭔가 잘못된건 확실한 듯 하다.

 

Guide | T MAP API

 

Guide | T MAP API

 

tmapapi.tmapmobility.com

여기에서 보행자 경로 안내를 들어가면 원하는 레퍼런스로 넘어갈 수 있다.

 

1) Query Params
- version (required)
2) Body Params
- startX (required)
- startY (required)
- endX (required)
- endY (required)
- startName (required)
- endName (required)

 

여기까지가 필수 사항들이다. 여기가 문제였구만! 이제까지는 startX, startY endX, endY만 계속 가져왔다...

게다가 Tmap api는 POST + JSON body 방식이라 기존에 사용했던 GET 방식 + 쿼리파라미터 방식이랑 차이가 있었기 때문에 mismatch가 생긴것이였다.

 

그 후에도 version 파라미터 때문에 생긴 네트워크 오류 client Exception도 있었는데 이건 클라이언트 단에서 버전을 가져오기 보단 서버에 애초부터 version 정보를 넣고 호출하게 해서 해결했다.


 

최종 실행결과 + 코드이다.

# main.py

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

# .env 불러오기
load_dotenv()

app = FastAPI()

# CORS 설정 (Flutter Web용)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 개발 단계에서만 *, 배포 시엔 도메인 지정 권장
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 환경변수에서 API 키 불러오기
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
TMAP_API_KEY = os.getenv("TMAP_API_KEY")

# 기본 루트 엔드포인트
@app.get("/")
def read_root():
    return {
        "message": "FastAPI is running!",
        "google_key_loaded": bool(GOOGLE_API_KEY),
        "tmap_key_loaded": bool(TMAP_API_KEY),
    }

# ✅ Google Directions API (driving / walking / transit)
@app.get("/directions")
def get_directions(
    origin: str = Query(...),
    destination: str = Query(...),
    mode: str = Query("driving")  # driving, walking, transit
):
    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()

class WalkRequest(BaseModel):
    startX: float
    startY: float
    endX: float
    endY: float
    startName: str
    endName: str

# ✅ Tmap 도보 경로 요청 API
@app.post("/tmap/walk")
def get_tmap_walk(body: WalkRequest = Body(...)):
    url = f"https://apis.openapi.sk.com/tmap/routes/pedestrian?version=1"
    headers = {
        "appKey": TMAP_API_KEY,
        "Content-Type": "application/json",
    }

    payload = {
        "startX": body.startX,
        "startY": body.startY,
        "endX": body.endX,
        "endY": body.endY,
        "startName": body.startName,
        "endName": body.endName,
        "reqCoordType": "WGS84GEO",
        "resCoordType": "WGS84GEO"
    }

    response = requests.post(url, headers=headers, json=payload)
    return response.json()
# travel_course_detail_screen.dart

try {
        // 자동차 시간 - URL을 명확하게 구성하고 인코딩
        final carUri = Uri.parse('http://localhost:8000/directions').replace(queryParameters: {
  'origin': '$startLat,$startLng',
  'destination': '$endLat,$endLng',
  'mode': 'driving',
});

        debugPrint("🚗 자동차 API 요청: ${carUri.toString()}");
        final carRes = await http.get(carUri); // FastAPI가 대신 Google API 호출
        debugPrint("자동차 API 응답 코드: ${carRes.statusCode}");

        // 도보 시간 - URL을 명확하게 구성하고 인코딩
        final walkUri = Uri.parse('http://localhost:8000/tmap/walk');
        
        debugPrint("🚶 도보 API 요청: ${walkUri.toString()}");
        final walkRes = await http.post(
  walkUri,
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({
    'startX': '$startLng',
    'startY': '$startLat',
    'endX': '$endLng',
    'endY': '$endLat',
    'startName': start['name'] ?? '출발지',
    'endName': end['name'] ?? '도착지',
  }),
);
        debugPrint("도보 API 응답 코드: ${walkRes.statusCode}");

        //  대중교통 시간 - URL을 명확하게 구성하고 인코딩
        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}");

        String carDuration = '정보 없음';
        String walkDuration = '정보 없음';
        String transitDuration = '정보 없음';

        // 자동차 응답 처리
        if (carRes.statusCode == 200) {
          debugPrint("✅ 길찾기 API 응답 성공");
          final carData = json.decode(carRes.body);
          if(carData['routes'] != null && carData['routes'].isNotEmpty) { 
            carDuration = carData['routes'][0]['legs'][0]['duration']['text'] ?? '정보 없음';
          } else {
            debugPrint("🚗 ZERO_RESULTS or route 없음");
          }
        } else {
          debugPrint("❌ 자동차 API 응답 실패: ${carRes.body}");
        }

        // 도보 응답 처리 (tmap)
        if (walkRes.statusCode == 200) {
  debugPrint("✅ 도보 API 응답 성공 (Tmap)");
  final walkData = json.decode(walkRes.body);

  //debugPrint("Tmap 도보 응답 원본: ${walkRes.body}");
  
  if (walkData['features'] != null && walkData['features'].isNotEmpty) {
    final properties = walkData['features'][0]['properties'];
    final totalTime = properties['totalTime']; // 단위: 초 (seconds)
    if (totalTime != null) {
      walkDuration = '${(totalTime / 60).round()}분'; // 분 단위로 표시
    } else {
      debugPrint("⚠️ totalTime 없음");
    }
  } else {
    debugPrint("🚶 Tmap 응답에 features 없음");
  }
        } else {
          debugPrint("❌ 도보 API 응답 실패: ${walkRes.body}");
        }

        // 대중교통 응답 처리
        if (transitRes.statusCode == 200) {
          debugPrint("✅ 대중교통 API 응답 성공");
          final transitData = json.decode(transitRes.body);
          if(transitData['routes'] != null && transitData['routes'].isNotEmpty) { 
            transitDuration = transitData['routes'][0]['legs'][0]['duration']['text'] ?? '정보 없음';
           } else {
            debugPrint("🚍 ZERO_RESULTS or route 없음");
          }
        
          times.add({
            'car': carDuration,
            'walk': walkDuration,
            'transit': transitDuration,
        });
          
          debugPrint("🕒 계산된 시간: 자동차 $carDuration분, 도보 $walkDuration분, 대중교통 $transitDuration분");
        } else {
          debugPrint("❌ 길찾기 API 응답 실패: 자동차(${carRes.statusCode}), 도보(${walkRes.statusCode}), 대중교통 (${transitRes.statusCode})");
          
          // 에러 응답 내용 확인
          if (carRes.statusCode != 200) {
            debugPrint("자동차 API 에러: ${carRes.body}");
          }
          if (walkRes.statusCode != 200) {
            debugPrint("도보 API 에러: ${walkRes.body}");
          }
          if (transitRes.statusCode != 200) {
            debugPrint("대중교통 API 에러: ${transitRes.body}");
          }
          
          times.add({
            'car': '통신 오류',
            'walk': '통신 오류',
            'transit': '통신 오류',
          });
        }
      } catch (e) {
        debugPrint("❌ 길찾기 API 호출 오류: $e");
        times.add({
          'car': '에러',
          'walk': '에러',
          'transit': '에러',
        });
        
        setState(() {
          apiError = '네트워크 오류: $e';
        });
      }
    }