2차 프로젝트 끝나자마자 바로 3차 프로젝트 시작!
2차는 아주 무사히 그리고 좋은 결과물로 발표하고 피드백도 받고 끝!!
(2차 프로젝트 리뷰는 따로 정리할 예정,,)
끝나자마자 해서 힘드냐고 물어보신다면
전~~~~~~~혀요? 오히려 기대하고 기다려지고 너무 신나고 재밌고 그런걸요??😭😭😭
3차 주제
바로바로~ 주유소/전기차 충전소 정보 제공 및 현재 위치 기준 목적지 추천 시스템!
주제 보자마자 떠올린게 지도를 이용하고 그 지도 안에서 할 수 있는 기능들은 다 써봐야겠다. 생각이 들었다.
이번 프로젝트에서는 일단 전기차 충전소 페이지를 맡았고 바로 시작!
2차때는 구글 맵 API를 썼지만 3차때는 카카오 맵 API로 진행하기로 했다.
전체적으로 지도를 펼치고 왼쪽에 가이드같이 해서 사용자들에게 제공하는 방식으로 시작!
(참고 사이트 여기서 다했습니다,,, 최고최고: https://velog.io/@hyunjoogo/카카오-Maps-API를-이용하여-경로-표시하기 )
초기 프로토타입 입니다!
먼저 충전소 검색 탭!
왼쪽 부분에서는 충전소 검색과, 길 찾기 기능을 제공할 예정이고
지역 선택을 통해 필터링 기능도 추가할 예정입니다.
검색이 완료 되면 하단에 리스트 형태로 충전소 이름과 충전기 타입을 제공할 예정이고
지도에 떠 있는 아이콘이나 결과값에 나온 리스트를 누르면 우측 상단에 팝업처럼 충전소 정보를 제공할 예정입니다.
다음은 충전소 길 찾기 탭 부분입니다.
사용자한테 현재 위치를 받거나, 입력받고
도착지도 입력받은 후 검색을 누르면 경로가 나오게 할 예정입니다.
일단 하면서 느낀거지만.. 카카오API는 참 쓰기 편하게 되어있는거 같다..
2차때 구글 쓰지말고 그냥 카카오 쓸껄 하는 생각이 백만번 들었다!
카카오 디벨로퍼 들어가서 애플리케이션 추가하고, 호스트 등록하고, API키값 받아오기 완료
<div class="kakaomap" id="map"></div>
카카오 맵 추가하고~
맵 관리할 kakaoevmap.js를 만들어줬다.
카카오MAP API 사이트에서 가이드랑 샘플 보면서 내가 구현해야할 기능들을 생각하면서 추가했다.
지도 타입, 줌 컨트롤
교통정보
장소 검색 객체
출발지, 도착지 마커
검색 시 필요한 자동완성
주소를 좌표로 변환하는 기능 등등 하면서 더 추가될 것 같지만 일단 이 정도로 추가했다.
그리고 검색하다 보니까 카카오 모빌리티 디벨로퍼에 길찾기 기능도 제공해주고 있었다. 이것도 추가!
// 지도 설정
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
// 지도 타입과 줌 컨트롤을 추가합니다
var mapTypeControl = new kakao.maps.MapTypeControl();
map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
var zoomControl = new kakao.maps.ZoomControl();
map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
// 교통정보를 지도에 표시
map.addOverlayMapTypeId(kakao.maps.MapTypeId.TRAFFIC);
// 장소 검색 객체 생성
var ps = new kakao.maps.services.Places();
// 출발지와 도착지 마커를 전역 변수로 생성해둠
var originMarker = new kakao.maps.Marker();
var destinationMarker = new kakao.maps.Marker({
image: new kakao.maps.MarkerImage(
'https://t1.daumcdn.net/localimg/localimages/07/2018/pc/flagImg/red_b.png', // 빨간색 마커 이미지 URL
new kakao.maps.Size(35, 40) // 마커 이미지의 크기
)
});
var polyline; // 경로를 표시할 폴리라인 객체
// 출발지 마커 설정 함수
function setOriginMarker(place) {
var coords = new kakao.maps.LatLng(place.y, place.x);
originMarker.setPosition(coords);
originMarker.setMap(map);
map.setCenter(coords);
}
// 도착지 마커 설정 함수
function setDestinationMarker(place) {
var coords = new kakao.maps.LatLng(place.y, place.x);
destinationMarker.setPosition(coords);
destinationMarker.setMap(map);
map.setCenter(coords);
}
// 자동완성 기능 및 장소 선택 함수
function autoComplete(query, suggestionsContainer, selectPlaceCallback) {
if (!query.trim()) {
suggestionsContainer.innerHTML = ""; // 입력이 없을 경우 목록을 지움
return;
}
ps.keywordSearch(query, function(data, status) {
if (status === kakao.maps.services.Status.OK) {
suggestionsContainer.innerHTML = ""; // 기존 목록을 초기화
data.forEach(function(place) {
const item = document.createElement("div");
item.className = "autocomplete-item";
item.textContent = place.place_name;
item.onclick = () => selectPlaceCallback(place);
suggestionsContainer.appendChild(item);
});
}
});
}
// 출발지 선택 시 처리 함수
function selectOriginPlace(place) {
document.getElementById("originInput").value = place.place_name;
document.getElementById("originCoords").value = `${place.x},${place.y}`;
originSuggestions.innerHTML = "";
setOriginMarker(place);
}
// 도착지 선택 시 처리 함수
function selectDestinationPlace(place) {
document.getElementById("destinationInput").value = place.place_name;
document.getElementById("destinationCoords").value = `${place.x},${place.y}`;
destinationSuggestions.innerHTML = "";
setDestinationMarker(place);
}
// 출발지와 목적지 자동완성 설정
document.getElementById('originInput').addEventListener('input', function() {
autoComplete(this.value, originSuggestions, selectOriginPlace);
});
document.getElementById('destinationInput').addEventListener('input', function() {
autoComplete(this.value, destinationSuggestions, selectDestinationPlace);
});
// 길찾기 버튼 클릭 시 경로 찾기
document.getElementById('searchButton').addEventListener('click', async function() {
const originCoords = document.getElementById("originCoords").value;
const destinationCoords = document.getElementById("destinationCoords").value;
if (originCoords && destinationCoords) {
const routeData = await getRoute(originCoords, destinationCoords);
if (routeData) displayRoute(routeData);
} else {
alert("출발지와 목적지를 모두 입력해 주세요.");
}
});
// 길찾기 API 호출 함수
async function getRoute(origin, destination) {
const apiKey = '서비스 키';
const url = `https://apis-navi.kakaomobility.com/v1/directions?origin=${origin}&destination=${destination}&priority=RECOMMEND&car_fuel=GASOLINE`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `KakaoAK ${apiKey}`,
'Content-Type': 'application/json'
}
});
// 응답 데이터를 JSON으로 변환
const routeData = await response.json();
// 전체 routeData 출력
console.log("Full Route Data:", routeData);
return routeData;
} catch (error) {
console.error("경로 요청 중 오류 발생:", error);
return null;
}
}
// 경로와 안내 지침 표시 함수
function displayRoute(routeData) {
if (routeData && routeData.routes && routeData.routes[0]) {
const linePath = [];
const directionsList = document.getElementById('directionsList'); // 경로 안내 표시할 div
directionsList.innerHTML = ''; // 기존 안내 초기화
// 총 예상 이동 시간 계산
const totalDurationInSeconds = routeData.routes[0].summary.duration;
const totalMinutes = Math.floor(totalDurationInSeconds / 60);
const totalSeconds = totalDurationInSeconds % 60;
const totalDurationText = `총 예상 이동 시간: 약 ${totalMinutes}분 `; //초는 이렇게 사용! ${totalSeconds}초
// 예상 이동 시간 표시
const totalDurationElement = document.createElement('div');
totalDurationElement.className = 'total-duration';
totalDurationElement.innerHTML = `<strong>${totalDurationText}</strong>`;
directionsList.appendChild(totalDurationElement);
// 경로 데이터에서 x, y 좌표를 추출하여 선을 구성
routeData.routes[0].sections[0].roads.forEach(road => {
const vertexes = road.vertexes;
for (let i = 0; i < vertexes.length; i += 2) {
linePath.push(new kakao.maps.LatLng(vertexes[i + 1], vertexes[i]));
}
});
// 각 가이드(guide) 지침을 화면에 표시
routeData.routes[0].sections[0].guides.forEach(guide => {
const directionItem = document.createElement('div');
directionItem.className = 'direction-item';
// 안내 문구 사용 (guidance 항목)
const guidanceText = guide.guidance || "이동";
// 지침과 거리 포함한 HTML로 설정
directionItem.innerHTML = `<strong>${guidanceText}</strong> 후 ${guide.distance}m 이동`;
directionsList.appendChild(directionItem);
});
// 기존 경로가 있다면 제거
if (polyline) {
polyline.setMap(null);
}
// 새로운 경로를 그리고 지도에 표시
polyline = new kakao.maps.Polyline({
path: linePath,
strokeWeight: 5,
strokeColor: '#FF0000',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
// 경로에 맞게 지도 범위 설정
const bounds = new kakao.maps.LatLngBounds();
linePath.forEach(point => bounds.extend(point));
map.setBounds(bounds);
} else {
alert("경로를 찾을 수 없습니다.");
}
}
// 지도에 마커를 표시하는 함수
function displayMarker(place) {
var marker = new kakao.maps.Marker({
map: map,
position: new kakao.maps.LatLng(place.y, place.x)
});
kakao.maps.event.addListener(marker, 'click', function() {
infowindow.setContent('<div style="padding:5px;font-size:12px;">' + place.place_name + '</div>');
infowindow.open(map, marker);
});
}
// 주소를 좌표로 변환하는 함수
async function getCoordinatesFromAddress(address) {
const apiKey = '서비스 키';
const url = `https://dapi.kakao.com/v2/local/search/address.json?query=${address}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Authorization': `KakaoAK ${apiKey}` }
});
const data = await response.json();
if (data.documents && data.documents.length > 0) {
return `${data.documents[0].x},${data.documents[0].y}`;
} else {
console.warn("좌표를 찾을 수 없습니다.");
return null;
}
} catch (error) {
console.error("주소 변환 중 오류 발생:", error);
return null;
}
}
초기 단계지만 2차 프로젝트에서 API를 많이 사용해본 경험이 이번 프로젝트에서 도움이 많이 되는거 같다.
거기다가 카카오에서 제공하는 api 값들이 사용하기 쉽게 설명이 되어있어서 사용하기 편했다.
(사용자 눈에 익숙한 UI/UX.. 왜 안써..?)
구현할 기능들
여러 기능들을 구현할 때 검색 자동완성 기능에서 조금 어려웠다.
자동완성을 어떤 식으로 구현해야 하는지 감이 안잡혀서 문서를 찾아보다가
카카오에서 제공하는 키워드로 장소 검색 API를 봤다.
https://apis.map.kakao.com/web/documentation/#services_Places_keywordSearch
그럼 사용자한테 실시간으로 값을 전달받아서(입력 폼)
키워드로 장소 검색 API로 보내고
결과값을 다시 받아서 출력하게 해서 선택하면 자동완성 기능이 될 것 같았다.
ps.keywordSearch 메서드를 이용해서 query에 해당하는 장소를 검색하고,
callback으로 검색 결과와 상태를 반환하게 했다.
그리고 status이 kakao.maps.services.Status.OK가 되면 장소 목록을 화면에 표시하게 했다.
각각의 장소(place) 에 대해서는 item 요소를 생성한 후 autocomplete-item 클래스를 추가, place_name을 표시하게 했다.
그러고 사용자가 item을 클릭하면 해당 장소 정보인 place를 selectPlaceCallback에 전달했다.
suggestionsContainer를 이용해서 <div id="suggestionsContainer">에 표시하게 했다.
// 자동완성 기능 및 장소 선택 함수
function autoComplete(query, suggestionsContainer, selectPlaceCallback) {
if (!query.trim()) {
suggestionsContainer.innerHTML = ""; // 입력이 없을 경우 목록을 지움
return;
}
ps.keywordSearch(query, function(data, status) {
if (status === kakao.maps.services.Status.OK) {
suggestionsContainer.innerHTML = ""; // 기존 목록을 초기화
data.forEach(function(place) {
const item = document.createElement("div");
item.className = "autocomplete-item";
item.textContent = place.place_name;
item.onclick = () => selectPlaceCallback(place);
suggestionsContainer.appendChild(item);
});
}
});
}
<!-- 충전소 길 찾기 내용 -->
<div id="routeContent" class="tab-content">
<div class="route-search">
<input type="text" id="originInput" placeholder="출발지를 입력하세요" class="route-input">
<div id="originSuggestions" class="suggestions-container"></div>
<input type="text" id="destinationInput" placeholder="도착지를 입력하세요" class="route-input">
<div id="destinationSuggestions" class="suggestions-container"></div>
<button id="searchButton" class="route-searchbutton">경로 검색</button>
<input type="hidden" id="originCoords">
<input type="hidden" id="destinationCoords">
</div>
자동완성 결과
좌측 상단에 사용자가 검색할 수 있는 부분을 뒀고
출발지를 입력하세요 부분에 출발지를 입력,
도작지를 입력하세요 부분에 도착지를 입력 후 경로 검색하면 해당 경로까지의 길이 나와야 한다.
또한 검색 시 자동완성 기능을 제공해야 한다.
자동완성 기능 테스트!
잘 된다! 출발지에 파란 마커, 도착지에는 빨간 도착 마커를 찍히게 했다.
이제 카카오 모빌리티에서 제공하는 길찾기API로 출발지, 도착지를 표시하면 된다!
사용한 api : 자동차 길찾기 API
https://developers.kakaomobility.com/docs/navi-api/directions/
// 길찾기 API 호출 함수
async function getRoute(origin, destination) {
const apiKey = '서비스 키';
const url = `https://apis-navi.kakaomobility.com/v1/directions?origin=${origin}&destination=${destination}&priority=RECOMMEND&car_fuel=GASOLINE`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `KakaoAK ${apiKey}`,
'Content-Type': 'application/json'
}
});
// 응답 데이터를 JSON으로 변환
const routeData = await response.json();
// 전체 routeData 출력
console.log("Full Route Data:", routeData);
return routeData;
} catch (error) {
console.error("경로 요청 중 오류 발생:", error);
return null;
}
}
// 경로와 안내 지침 표시 함수
function displayRoute(routeData) {
if (routeData && routeData.routes && routeData.routes[0]) {
const linePath = [];
const directionsList = document.getElementById('directionsList'); // 경로 안내 표시할 div
directionsList.innerHTML = ''; // 기존 안내 초기화
// 총 예상 이동 시간 계산
const totalDurationInSeconds = routeData.routes[0].summary.duration;
const totalMinutes = Math.floor(totalDurationInSeconds / 60);
const totalSeconds = totalDurationInSeconds % 60;
const totalDurationText = `총 예상 이동 시간: 약 ${totalMinutes}분 `; //초는 이렇게 사용! ${totalSeconds}초
// 예상 이동 시간 표시
const totalDurationElement = document.createElement('div');
totalDurationElement.className = 'total-duration';
totalDurationElement.innerHTML = `<strong>${totalDurationText}</strong>`;
directionsList.appendChild(totalDurationElement);
// 경로 데이터에서 x, y 좌표를 추출하여 선을 구성
routeData.routes[0].sections[0].roads.forEach(road => {
const vertexes = road.vertexes;
for (let i = 0; i < vertexes.length; i += 2) {
linePath.push(new kakao.maps.LatLng(vertexes[i + 1], vertexes[i]));
}
});
// 각 가이드(guide) 지침을 화면에 표시
routeData.routes[0].sections[0].guides.forEach(guide => {
const directionItem = document.createElement('div');
directionItem.className = 'direction-item';
// 안내 문구 사용 (guidance 항목)
const guidanceText = guide.guidance || "이동";
// 지침과 거리 포함한 HTML로 설정
directionItem.innerHTML = `<strong>${guidanceText}</strong> 후 ${guide.distance}m 이동`;
directionsList.appendChild(directionItem);
});
// 기존 경로가 있다면 제거
if (polyline) {
polyline.setMap(null);
}
// 새로운 경로를 그리고 지도에 표시
polyline = new kakao.maps.Polyline({
path: linePath,
strokeWeight: 5,
strokeColor: '#FF0000',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
// 경로에 맞게 지도 범위 설정
const bounds = new kakao.maps.LatLngBounds();
linePath.forEach(point => bounds.extend(point));
map.setBounds(bounds);
} else {
alert("경로를 찾을 수 없습니다.");
}
}
일단 출발지와 목적지 좌표인 origin, destination을 통해서 kakao 길찾기 api한테 get 요청을 보내고
응답 데이터인 routeData를 JSON 형식으로 받았다.
총 예상 이동 시간 계산하기
먼저 총 예상 이동 시간을 계산하고 표시하기 위해
// 총 예상 이동 시간 계산 및 표시
const totalDurationInSeconds = routeData.routes[0].summary.duration;
const totalMinutes = Math.floor(totalDurationInSeconds / 60); // 분으로 변환
const totalSeconds = totalDurationInSeconds % 60; // 초 단위의 나머지 계산
const totalDurationText = `총 예상 이동 시간: 약 ${totalMinutes}분`; // 예상 이동 시간 텍스트
이렇게 작성했고 totalDurationInSeconds 변수에 총 예상 이동시간을 초 단위로 제공하기 때문에 초 단위로 받았다.
그러고 초 단위를 분 단위로 변환하기 위해 totalDurationInSeconds를 60으로 나눴고
Math.floor로 정수만 받은 후
totalDurationText에 총 예상 이동 시간을 문자열로 받아서 저장했다. 그러면 약 : OO분 이렇게 나오게 된다!
출발지, 도착지 경로 선 구성하기
다음으로는 출발지 마커와 도착지 마커에 대해 선을 그려서 사용자들이 보기 편하게 구성해야 했다.
자동차 길 찾기니까 출발지와 도착지까지 일직선으로 선을 그을 수 없고, 네비게이션처럼 길 따라 선이 그어지게 해야 한다.
이렇게 routes, sections, roads 등등 필요한 값들을 받았다.
routeData.routes[0].sections[0].roads
이 값으로 도로를 따라 경로를 나타내는 도로 구간 정보 배열을 만들었다.
각각의 road 객체에는 vertexes라는 배열이 있고 이 vertexes 배열에는 도로 경로를 따라 가는 좌표 데이터가 순서대로 포함되어 있어서 그걸 사용했다.
그러고 for 반복문을 통해 vertexes 배열을 두 칸씩 건너뛰면서 x, y 좌표를 추출해서 카카오맵에서 사용하는 LatLng 객체로 변환하고 linePath 배열에 추가했다.
routeData.routes[0].sections[0].roads.forEach(road => {
const vertexes = road.vertexes;
for (let i = 0; i < vertexes.length; i += 2) {
linePath.push(new kakao.maps.LatLng(vertexes[i + 1], vertexes[i]));
}
});
이렇게 만들어진 linePath 배열에는 출발지 → 도착지 간의 모든 경로의 좌표가 도로에 맞게 저장된다!
처음에는 선을 그으면 일직선으로 공중에 그어질 줄 알았는데 길찾기API에서 생각보다 많은 값들을 제공하고 있어서 편하게 받아와서 이어주기만 하면 됐다. 길찾기 API에서 실제 도로 정보를 포함한 좌표 데이터를 주고 있기 때문에 그것만 잘 사용하면 됐다. (특히 vertexes 배열)
// 경로 데이터에서 x, y 좌표를 추출하여 선을 구성
routeData.routes[0].sections[0].roads.forEach(road => {
const vertexes = road.vertexes;
for (let i = 0; i < vertexes.length; i += 2) {
linePath.push(new kakao.maps.LatLng(vertexes[i + 1], vertexes[i]));
}
});
// 기존 경로가 있다면 제거
if (polyline) {
polyline.setMap(null);
}
// 새로운 경로를 그리고 지도에 표시
polyline = new kakao.maps.Polyline({
path: linePath,
strokeWeight: 5,
strokeColor: '#FF0000',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
// 경로에 맞게 지도 범위 설정
const bounds = new kakao.maps.LatLngBounds();
linePath.forEach(point => bounds.extend(point));
map.setBounds(bounds);
} else {
alert("경로를 찾을 수 없습니다.");
}
경로 설명 제공하기
이제 경로에 대한 설명을 네비게이션처럼 제공하고 싶어서 guides와 guidance를 사용했다.
routeData.routes[0].sections[0].guides를 통해 경로 안내를 만들어봤다!
routes에는 최상위 경로 목록이 담겨있고
sections에는 routes배열 안에 있는 값이며, 경로의 특정 구간을 나타낸다.
한 경로에 여러 구간이 있을 수도 있으니까!
(A도시에서 B도시로 이동하는데 C도시를 지나가는 경우 A→C sections과, C→B의 sections값이 있는 것!)
각 section에는 구간에서의 도로 정보와 안내 지침 정보가 들어있다.
guides는 좀 달랐다. sections[] 배열이 아닌 sections 객체의 필드로 존재하고
사용자에게 제공될 길 안내 지침을 제공했다.
sections[0] 구간을 따라서 이동하는 동안 특정 지점에서의 상세 안내 정보를 제공했다.
(회전 지점, 진행 방향, 거리 등등)
routeData.routes[0].sections[0].guides.forEach(guide => {
const directionItem = document.createElement('div');
directionItem.className = 'direction-item';
const guidanceText = guide.guidance || "이동";
directionItem.innerHTML = `<strong>${guidanceText}</strong> 후 ${guide.distance}m 이동`;
directionsList.appendChild(directionItem);
});
최종 결과
이제 다음 작업은
항상 목적지 검색하면
출발지 후 0m 이동 이라고 뜬다.. 이 부분 해결,
우회전 좌회전 등등에 이미지 추가, 구성 바꾸기
출발지, 도착지 위치 스왑 기능 넣기
충전소 길 찾기니까 충전소 검색 탭에서 받은 충전소 위치들을 길 찾기 탭으로 넘기기 등등등
할게 많다!
'Develop > JPA' 카테고리의 다른 글
전기차 충전소 길찾기 (5) (0) | 2024.11.13 |
---|---|
전기차 충전소 API 상세 정보 팝업 (4) (0) | 2024.11.12 |
전기차 충전소 API 데이터 가공하기 (3) (2) | 2024.11.11 |
전기차 충전소 API 호출하기 (2) (0) | 2024.11.08 |
전기차 충전소 API 호출하기 (1) (0) | 2024.11.07 |