텐센트 지도 API와 DeepSeek LLM을 결합한 실시간 지능형 여행 플래너 구현

대형 언어 모델(LLM)을 활용한 여행 계획 생성은 대중적인 프로젝트 주제입니다. 하지만 단순히 텍스트 생성에만 의존할 경우, 존재하지 않는 식당을 추천하거나 이동 거리를 고려하지 않는 등의 '할루시네이션(환각)' 문제가 발생합니다. 이 글에서는 텐센트 지도(Tencent Map)의 실제 POI(Point of Interest) 데이터와 DeepSeek 모델을 결합하여, 실제 실행 가능한 경로와 장소를 제안하는 지능형 여행 플래너의 구현 과정을 설명합니다.

1. 시스템 아키텍처 및 설계 개념

핵심 아이디어는 AI가 여행 계획을 짜기 전, 지도 API를 통해 목적지의 실시간 데이터를 먼저 검색하고 이 정보를 프롬프트에 주입하는 것입니다. 전체 워크플로우는 다음과 같습니다.

  1. 지오코딩: 사용자 목적지를 좌표(위도, 경도)로 변환.
  2. 키워드 추출: DeepSeek을 통해 사용자 요구사항에서 검색 키워드(명소, 맛집, 호텔 등)를 도출.
  3. 데이터 수집: 텐센트 지도 WebService API로 실제 POI 정보 및 장소 간 경로/소요 시간 획득.
  4. 프롬프트 주입: 수집된 실제 데이터를 AI 모델의 컨텍스트로 전달.
  5. 스트리밍 응답: SSE(Server-Sent Events)를 통해 지도 마커 데이터와 생성된 텍스트를 실시간으로 클라이언트에 전송.

2. 백엔드 구현: 데이터 엔진 및 AI 통합

FastAPI를 사용하여 비동기 통신을 지원하고, 텐센트 지도 API 호출과 AI 추론을 결합합니다.

2.1 텐센트 지도 API 연동 모듈

지도 서비스의 핵심 기능을 MapProvider 클래스로 캡슐화합니다. API 호출 시 예외 처리를 강화하여 특정 API 실패가 전체 프로세스를 중단시키지 않도록 설계합니다.

import httpx
import os

class MapProvider:
    def __init__(self):
        self.api_key = os.getenv("TENCENT_MAP_KEY")
        self.base_url = "https://apis.map.qq.com/ws"

    async def fetch_coordinates(self, address: str):
        """주소를 좌표로 변환"""
        url = f"{self.base_url}/geocoder/v1/"
        params = {"address": address, "key": self.api_key}
        async with httpx.AsyncClient() as client:
            resp = await client.get(url, params=params)
            result = resp.json()
            if result.get("status") == 0:
                return result["result"]["location"]
            return None

    async def search_nearby_places(self, query: str, boundary: str):
        """주변 POI 검색"""
        url = f"{self.base_url}/place/v1/search"
        params = {
            "keyword": query,
            "boundary": boundary,
            "key": self.api_key,
            "page_size": 5
        }
        async with httpx.AsyncClient() as client:
            resp = await client.get(url, params=params)
            data = resp.json()
            return data.get("data", []) if data.get("status") == 0 else []

2.2 DeepSeek을 활용한 검색 전략 수립

사용자의 자연어 입력을 지도 검색에 적합한 구조화된 데이터로 변환합니다. 이때 낮은 temperature(0.3) 설정을 사용하여 JSON 출력의 안정성을 높입니다.

async def generate_search_keywords(user_input: str):
    prompt = f"""
    사용자의 여행 요청: "{user_input}"
    위 요청을 바탕으로 지도 검색을 위한 키워드를 JSON 배열로 추출하세요.
    형식: [{"keyword": "명칭", "category": "attraction|food|hotel", "limit": 5}]
    """
    # DeepSeek API 호출 로직 (생략)
    # 결과가 JSON 형식이 아닐 경우를 대비한 파싱 및 샌드박스 처리 필수

2.3 리얼 데이터 기반 프롬프트 구성

지도 API에서 가져온 실제 정보를 AI가 읽기 쉬운 텍스트 형식으로 변환하여 최종 프롬프트에 결합합니다.

def build_context_with_poi(poi_list, routes):
    context = "### 검색된 실제 장소 정보\n"
    for poi in poi_list:
        context += f"- {poi['title']}: {poi['address']} (카테고리: {poi['category']})\n"
    
    context += "\n### 이동 거리 참고\n"
    for route in routes:
        dist = route['distance'] / 1000
        time = route['duration'] / 60
        context += f"- {route['origin']} -> {route['dest']}: 약 {dist:.1f}km, {time:.0f}분 소요\n"
    
    return context

3. 프론트엔드: 지도 시각화 및 인터랙션

텐센트 지도 JS API GL 버전을 사용하여 동적인 마커 표시와 경로 렌더링을 구현합니다.

3.1 지도 레이어 초기화

Vue 3 환경에서 지도를 초기화하고, 명소, 식당, 호텔별로 다른 스타일의 마커를 설정합니다.

const initMapInstance = (center) => {
  const map = new TMap.Map('map-container', {
    center: new TMap.LatLng(center.lat, center.lng),
    zoom: 12
  });

  const markerLayer = new TMap.MultiMarker({
    map: map,
    styles: {
      'attraction': new TMap.MarkerStyle({ width: 25, height: 35, src: '/icons/red-pin.png' }),
      'food': new TMap.MarkerStyle({ width: 25, height: 35, src: '/icons/orange-pin.png' }),
      'hotel': new TMap.MarkerStyle({ width: 25, height: 35, src: '/icons/blue-pin.png' })
    }
  });

  return { map, markerLayer };
};

3.2 경로(Polyline) 렌더링 주의사항

텐센트 경로 계획 API가 반환하는 좌표 형식은 [lng, lat] 순서이지만, JS SDK의 LatLng 객체는 (lat, lng) 순서를 요구합니다. 데이터 처리 시 이 순서를 뒤집어주어야 경로가 정상적으로 표시됩니다.

function drawRoutes(apiRoutes) {
  const geometries = apiRoutes.map(route => ({
    paths: route.polyline.map(p => new TMap.LatLng(p[1], p[0])), // 순서 변환
    styleId: 'route-style'
  }));
  
  polylineLayer.setGeometries(geometries);
}

4. 실전 개발 팁 및 트러블슈팅

  • API 할당량 관리: 텐센트 지도 무료 티어는 일일 호출 제한이 있습니다. POI 검색 시 asyncio.gather를 사용하여 병렬 처리를 하되, 불필요한 중복 호출을 줄이기 위해 검색 결과를 캐싱하는 것이 좋습니다.
  • SSE 통신 안정성: 대량의 마커 데이터를 SSE로 보낼 때 JSON 데이터가 잘리지 않도록 ensure_ascii=False 설정과 적절한 버퍼 처리가 필요합니다.
  • 지오코딩 실패 대응: 사용자가 모호한 도시명을 입력할 경우 지오코딩이 실패할 수 있습니다. 이때는 기본 좌표를 설정하거나 사용자에게 위치 재확인을 요청하는 예외 처리를 추가해야 합니다.
  • LLM JSON 파싱: DeepSeek 모델이 응답에 마크다운 코드 블록(```json ... ```)을 포함하는 경우가 많습니다. 백엔드에서 정규식이나 문자열 처리를 통해 순수 JSON만 추출하는 유틸리티 함수가 필수적입니다.

이와 같은 방식을 통해 제작된 여행 플래너는 AI의 창의성과 지도 데이터의 정확성을 동시에 확보할 수 있습니다. 사용자는 텍스트로 된 일정뿐만 아니라 지도를 통해 동선을 즉시 확인할 수 있어 훨씬 더 신뢰도 높은 경험을 얻게 됩니다.

태그: Tencent-Maps DeepSeek FastAPI vue3 LLM

7월 19일 02:50에 게시됨