ChatGPT API의 응답을 데이터화 한 뒤, TMap API의 파라미터로 사용함으로써 아래와 같이 대화만으로도 핀이 자동 생성되는 기능을 만들었다.
생성형 AI가 추천해준 여행지의 위치를 확인하고, 추가버튼을 클릭함으로써 내 여행계획에 추가할 수 있는 기능인것이다.
이를 위해 이전 게시물에서 응답 데이터화를 마쳤고, 이번 포스팅에선 Tmap API를 사용하는 방법 및 위 기능을 구현하는 방법을 다루고자 한다. 카카오맵과 다르게 TMap은 프레임워크별로 코드를 제공하지 않는다. 바닐라JS 코드만 제공하기 때문에, Vue.js에서 활용하기 위해선 직접 커스텀 하는 작업이 필수적이었다.
Vue3.js에서 TMap을 사용하는 방법
우선 공식 문서는 다음에서 볼 수 있다. 공식문서에선 초기에 맵을 불러오는 코드를 다음과 같이 설명하고 있다.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>simpleMap</title>
<script src="https://apis.openapi.sk.com/tmap/jsv2?version=1&appKey=발급받은 App key"></script>
<script type="text/javascript">
function initTmap(){
var map = new Tmapv2.Map("map_div",
{
center: new Tmapv2.LatLng(37.566481622437934,126.98502302169841), // 지도 초기 좌표
width: "890px",
height: "400px",
zoom: 15
});
}
</script>
</head>
<body onload="initTmap()">
<div id="map_div">
</div>
</body>
</html>
$(document).ready(function() {
initTmap();
});
그러나 Vue.js와 같은 FE 프레임워크에선 바닐라JS를 사용하는 위 코드와 달리 리얼돔이 아닌 가상돔을 통해 데이터에 접근한다. 또한 eventListener를 만드는 방식도 다르고, var를 사용하지 않으며, 컴포넌트 단위로 각 파일을 설계하게 되기 때문에 처음엔 좀 당황스러웠다.
그러나 잘 생각해보면, 결국 template 내부에 map_div라는 이름의 div를 만들고, script 태그안에 함수를 만들어 두면 된다는것도 떠올릴 수 있다. 즉, vue에서 쓸수있는 코드는 다음과 같이 변환할 수 있다.
<script setup>
import { ref, computed, onMounted } from "vue";
onMounted(() => {
initTmap();
});
// 1. 지도 띄우기
const initTmap = () => {
map.value = new Tmapv2.Map("map_div", {
center: new Tmapv2.LatLng(37.5652045, 126.98702028),
width: "100%",
height: "400px",
zoom: 17,
zoomControl: true,
scrollwheel: true,
});
};
</script>
<template>
<section class="main-container">
<!-- 생략... -->
<!-- 맵 -->
<div id="map_div" class="map_wrap"></div>
해당 컴포넌트가 마운트 되는 순간 map을 초기화하는 함수를 실행하는것이다. 그리고 이 초기화 함수가 아까 예제의 script 태그 안에 들어가도록 구조화 한다. 나는 zoom이나 wheel같은 기능을 지원할것이므로, 설정을 추가해줬다.
Marker를 띄우는 방법 - POI 통합검색
marker를 띄우기 위해선 검색창에 검색어를 입력하거나, 또는 생성형 AI와 대화하거나, 또는 포스팅 하지 않았지만 다른사람의 여행기록을 가져오는 방법이 있다. 이를 구분하기 위해서 if문을 통해 각 경우를 구분한 뒤, POI통합검색 API를 활용한다. AI응답에서 추출한 데이터 객체 리스트의 반복문을 돌면서, 각 장소에 대해 좌표를 검색하는것이다. (여러개의 장소를 한번에 검색하는 API는 없기 때문에, 반복문을 통해 결과를 찾아 리스트에 저장하는 형태로 만들었다)
const getMapResult = async (cnt, keywords) => {
const url =
"https://apis.openapi.sk.com/tmap/pois?version=1&format=json&callback=result";
//생성형AI외 다른방법 코드는 생략
resultpoisData.value = [];
for (let keyword of keywords) {
const resp = await axios.get(url, {
headers: {
appKey: import.meta.env.VITE_T_MAP_SERVICE_KEY,
},
params: {
searchKeyword: keyword, // 검색 키워드
resCoordType: "EPSG3857", // 요청 좌표계
reqCoordType: "WGS84GEO", // 응답 좌표계
count: cnt, // 가져올 갯수
},
});
console.log(resp.data, resp.data == null, resp.data == "");
if (resp.data == null || resp.data == "") continue;
resultpoisData.value.push(resp.data.searchPoiInfo.pois.poi[0]);
}
// 기존 마커, 팝업 제거
if (markerArr.value.length > 0) {
for (let marker of markerArr.value) {
marker.setMap(null);
}
markerArr.value = [];
}
if (labelArr.value.length > 0) {
for (var i in labelArr.value) {
labelArr.value[i].setMap(null);
}
labelArr.value = [];
}
const positionBounds = new Tmapv2.LatLngBounds();
// POI 마커 표시
for (let k in resultpoisData.value) {
// POI 마커 정보 저장
const noorLat = Number(resultpoisData.value[k].noorLat);
const noorLon = Number(resultpoisData.value[k].noorLon);
const name = resultpoisData.value[k].name;
// 좌표 객체 생성
const pointCng = new Tmapv2.Point(noorLon, noorLat);
// EPSG3857좌표계를 WGS84GEO좌표계로 변환
const projectionCng = new Tmapv2.Projection.convertEPSG3857ToWGS84GEO(
pointCng
);
const lat = projectionCng._lat;
const lon = projectionCng._lng;
// 좌표 설정
const markerPosition = new Tmapv2.LatLng(lat, lon);
// Marker 설정
marker.value = new Tmapv2.Marker({
position: markerPosition, // 마커가 표시될 좌표
//icon : "/upload/tmap/marker/pin_b_m_a.png",
icon: "/img/marker.png", // 아이콘 등록
iconSize: new Tmapv2.Size(24, 24), // 아이콘 크기 설정
title: name, // 마커 타이틀
map: map.value, // 마커가 등록될 지도 객체
});
// 마커들을 담을 배열에 마커 저장
markerArr.value.push(marker.value);
positionBounds.extend(markerPosition); // LatLngBounds의 객체 확장
}
map.value.panToBounds(positionBounds); // 확장된 bounds의 중심으로 이동시키기
map.value.zoomOut();
getInfoPlace();
};
각 장소에 대한 세부정보를 가져오는 방법
지도에 마커를 전부 생성했다면, 이번엔 카드에 각 장소에 대한 세부정보를 담을 차례다.
POI API에서 상세보기 요청은 아주 간단하다. URL의 쿼리스트링에서 POI _ID값만 추가해주면 된다.
const getInfoPlace = async () => {
detailInfo.value = []; //초기화
for (const pois of resultpoisData.value) {
let url =
"https://apis.openapi.sk.com/tmap/pois/" +
pois.id + // 상세보기를 누른 아이템의 POI ID
"?version=1&resCoordType=EPSG3857&format=json&callback=result";
const { data } = await axios.get(url, {
headers: { appKey: import.meta.env.VITE_T_MAP_SERVICE_KEY },
});
detailInfo.value.push(data.poiDetailInfo);
}
};
주제와는 떨어진 것 같아 자세히 작성하진 않았지만, 왼쪽(검색창,지도,카드)영역은 하나로 묶여 Map컴포넌트로 따로 작성한것이므로, 채팅창 등 다른 기능과 결합시키기 위해 pinia와 watch, emit을 적극 활용해서 구현했다. 따라서 위 컴포넌트는 직접 계획작성하기, 다른사람의 계획 가져오기 등 다양한 기능에서 범용성있게 사용할 수 있다.
이렇게 바닐라JS로 제공되는 Tmap API를 Vue.js 버전으로 바꾸고, 기능명세에 적합하게 API를 활용해보았다. 사실 <WanderWay>는 Vue를 처음 도입해본 프로젝트였다. 당시 나는 JSP, Thymleaf만 경험해 봤기에 '가상돔'이나, 컴포넌트 단위의 설계가 생소하고 어렵게 느껴졌다. 그러나 이렇게 Vue버전의 코드를 제공하지 않는 API를 직접 변환해보면서 프론트엔드 프레임워크의 동작과정을 이해하고, 빠르게 익혀 개발할 수 있었다! 이를 양분삼아 1년이 지난 현재도 프론트는 Vue를 사용해서 효율적으로 개발하고있다ㅎㅎ
자세한 코드는 여기서 볼 수 있다!
'프로젝트 > 원더웨이' 카테고리의 다른 글
정규표현식&프롬프트 엔지니어링으로 생성형 AI응답을 객체화 하기 (9) | 2024.11.09 |
---|---|
QueryDSL 동적쿼리로 편리한 검색기능 만들기 (12) | 2024.11.08 |