본문 바로가기
Develop/JPA

3차 프로젝트 시작!

by ys2ys2 2024. 11. 6.

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

 

 

그럼 사용자한테 실시간으로 값을 전달받아서(입력 폼)

키워드로 장소 검색 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분 이렇게 나오게 된다!

 


 

 

출발지, 도착지 경로 선 구성하기

 

다음으로는 출발지 마커와 도착지 마커에 대해 선을 그려서 사용자들이 보기 편하게 구성해야 했다.

자동차 길 찾기니까 출발지와 도착지까지 일직선으로 선을 그을 수 없고, 네비게이션처럼 길 따라 선이 그어지게 해야 한다.

길찾기 API에서 제공하는 경로 정보

 

sections

 

roads

 

이렇게 routes, sections, roads 등등 필요한 값들을 받았다.

 

routeData.routes[0].sections[0].roads

이 값으로 도로를 따라 경로를 나타내는 도로 구간 정보 배열을 만들었다.

각각의 road 객체에는 vertexes라는 배열이 있고 이 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를 사용했다.

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 이동 이라고 뜬다.. 이 부분 해결,

우회전 좌회전 등등에 이미지 추가, 구성 바꾸기

출발지, 도착지 위치 스왑 기능 넣기

충전소 길 찾기니까 충전소 검색 탭에서 받은 충전소 위치들을 길 찾기 탭으로 넘기기 등등등

할게 많다!