본문 바로가기
프로젝트/RanDrive

Random-Drive-Project 기본 MVC 구현(경로 조회, 상세조회)

by HWK 2023. 10. 18.

경로 저장이 완료되었으니 경로 전체 조회, 상세 조회를 구현해야 한다.

경로 저장에 대한 설명은 아래 링크에 있다.

https://hwk99.tistory.com/117

 

53일차 - Random-Drive-Project 기본 MVC 구현(경로 저장)

경로를 저장하는 이유는 해당 유저가 다녀온 길을 다시 보여주고 안내하기 위해서이다. 고로 다시 경로를 보여주는데 필요한 데이터만 쓰기 편한 형식으로 저장하면 된다. 이전의 카카오에서

hwk99.tistory.com

 

경로 전체 조회

모든 조회할 것이고, 이전에 만들어 둔 Route entity 클래스를 이용하면 된다.

유저 정보를 이용해 해당 유저의 Route DB의 저장된 정보를 불러올 것이다.

 

1.Service

Service 코드는 AllHistoryResponseDto 형태로 나가도록 했다. 출발지 주소와 목적지 주소를 포함해야 하기 때문이다.

public List<AllHistoryResponseDto> getAllHistories(Long userId) {
    List<Route> routes = routeRepository.findAllByUserId(userId);

    return routes.stream()
            .map(route -> new AllHistoryResponseDto(
                    route.getId(),
                    route.getOriginAddress(),
                    route.getDestinationAddress(),
                    route.getDuration(),
                    route.getDistance(),
                    route.getCreatedAt()
            ))
            .collect(Collectors.toList());
}

2. controller

RestTemplate를 사용중이기 때문에 아래와 같은 형태로 정보를 보내준다.

@GetMapping("/routes")
public ResponseEntity<List<AllHistoryResponseDto>> getAllRoutes(@AuthenticationPrincipal UserDetailsImpl userDetails) {
    List<AllHistoryResponseDto> historyList = historyService.getAllHistories(userDetails.getUser().getId());
    return ResponseEntity.ok(historyList);
}

3. html

html 코드에 js 코드까지 함께 써 주었다.

경로 상세 보기로 넘어가는 버튼도 구현되어 있는데 그 부분은 이후에 설명하겠다.
어쨋든 얻은 정보들을 원하는 형식으로 나열해준다. 위에서 보여준것 처럼 UserDetails를 이용해야 하는데,

그 과정에서 Cookie를 header에 담아주는 과정이 필요하고, 해당 설명은 위에서 공유한 블로그 링크에 있다.

더보기
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>All Histories</title>
</head>
<body>
<h1>All User Histories</h1>

<table>
    <thead>
    <tr>
        <th>Route ID</th>
        <th>Origin Address</th>
        <th>Destination Address</th>
        <th>Duration</th>
        <th>Distance</th>
        <th>Created At</th>
        <th>Actions</th>
    </tr>
    </thead>
    <tbody id="historyTableBody">
    <!-- 여기에 데이터가 채워질 것입니다 -->
    </tbody>
</table>

<script src="https://code.jquery.com/jquery-3.7.0.min.js"
        integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>

<script>
    const auth = getToken();

    if (auth !== undefined && auth !== '') {
        $.ajaxPrefilter(function(options, originalOptions, jqXHR) {
            jqXHR.setRequestHeader('Authorization', auth);
        });

        // API 호출
        fetch('/api/routes', {
            method: 'GET',
            headers: {
                'Authorization': auth
            }})
            .then(response => response.json())
            .then(data => {
                // 받아온 데이터를 화면에 렌더링
                renderHistory(data);
            })
            .catch(error => {
                console.error('Error:', error);
            });
    } else {
        window.location.href = host + '/api/user/login-page';
    }

    function getToken() {

        let auth = Cookies.get('Authorization');

        if(auth === undefined) {
            return '';
        }

        // kakao 로그인 사용한 경우 Bearer 추가
        if(auth.indexOf('Bearer') === -1 && auth !== ''){
            auth = 'Bearer ' + auth;
        }
        return auth;
    }

    function renderHistory(histories) {
        const tbody = document.getElementById('historyTableBody');

        histories.forEach(history => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${history.route_id}</td>
                <td>${history.originAddress}</td>
                <td>${history.destinationAddress}</td>
                <td>${history.duration}</td>
                <td>${history.distance}</td>
                <td>${history.createdAt}</td>
                <td></td> <!-- Actions 열의 빈 칸 -->
            `;

            // 상세 정보 버튼을 생성하고 Actions 열에 추가
            const detailButton = addDetailButton(history.route_id);
            row.lastElementChild.appendChild(detailButton);

            tbody.appendChild(row);
        });
    }

    function addDetailButton(routeId) {
        const button = document.createElement('button');
        button.textContent = '상세 정보 보기';
        button.addEventListener('click', function() {
            // 상세 정보 페이지로 이동
            window.location.href = `/history/${routeId}`;
        });
        return button;
    }
</script>

</body>
</html>

이렇게 전체 조회가 완료되었다.

상세 조회로 넘어가보자.

 

경로 상세 조회

위 html코드에서 볼 수 있듯이, 버튼을 누르면 상세 페이지로 넘어간다.

routeId를 넘겨주는 방식이다.

먼저 해당 routeId에 맞는 Entity 정보들을 HistoryResponseDto 형태로 담아서 프론트쪽으로 보내줘야 한다.

 

1. Dto

HistoryResponseDto를 구현한 방식을 먼저 보여주겠다.

목적지 정보등을 담은 이유는 당장 필요해서가 아닌, 나중에 프론트 발전시 필요한 정보들을 미리 넣어두었다.

vertexes를 double[ ] 형으로 저장하고 있어서 entity에 String 형으로 저장된 정보를 변환해서 넣어줘야 한다.

package com.example.randomdriveproject.history.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class HistoryResponseDto {

    @JsonProperty("originAddress")
    private String originAddress;

    @JsonProperty("destinationAddress")
    private String destinationAddress;

    @JsonProperty("duration")
    private int duration;

    @JsonProperty("distance")
    private int distance;

    @JsonProperty("createdAt")
    private LocalDateTime createdAt;

    @JsonProperty("bounds")
    private Bound bounds;

    @JsonProperty("roads")
    private List<Road> roads = new ArrayList<>();

    public static class Bound {
        @JsonProperty("min_x")
        private double minX;

        @JsonProperty("min_y")
        private double minY;

        @JsonProperty("max_x")
        private double maxX;

        @JsonProperty("max_y")
        private double maxY;

        public Bound(double minX, double minY, double maxX, double maxY) {
            this.minX = minX;
            this.minY = minY;
            this.maxX = maxX;
            this.maxY = maxY;
        }

        // Getter, Setter, toString 등 필요한 메서드들은 생략
    }

    public static class Road {
        @JsonProperty("vertexes")
        private double[] vertexes;

        public Road(double[] vertexes) {
            this.vertexes = vertexes;
        }

        // Getter, Setter, toString 등 필요한 메서드들은 생략
    }

}

2. Service

저장된 Entity 정보들을 활용해 위의 Dto를 만들어 반환해 주었다.

convertStringToList라는 메서드를 설계해서 String 형을 double[ ]형을 변환해주었고,

그 결과 KakaoRouteAllResponseDto에서 필요한 정보만 빼온 HistoryResponseDto의 형태로 반환을 할 수 있었다.

public HistoryResponseDto getHistory(Long routeId) {
    // RouteId를 이용해 Route 정보를 가져온다고 가정
    Route route = routeRepository.findById(routeId).orElse(null);

    if (route == null) {
        // 해당 routeId에 대한 정보가 없을 경우 예외 처리 또는 적절한 응답을 반환
        throw new IllegalArgumentException("Route not found with ID: " + routeId);
    }

    // Bound, Road 정보 가져오기
    Bound bound = route.getBounds().get(0); // 예시로 첫 번째 Bound 가져옴
    List<Road> roads = route.getRoads();

    // HistoryResponseDto 객체 생성 및 설정
    HistoryResponseDto.Bound boundDto = new HistoryResponseDto.Bound(bound.getMinX(), bound.getMinY(), bound.getMaxX(), bound.getMaxY());

    HistoryResponseDto responseDto = new HistoryResponseDto();
    responseDto.setOriginAddress(route.getOriginAddress());
    responseDto.setDestinationAddress(route.getDestinationAddress());
    responseDto.setDuration(route.getDuration());
    responseDto.setDistance(route.getDistance());
    responseDto.setCreatedAt(route.getCreatedAt());
    responseDto.setBounds(boundDto);

    for (Road road : roads) {
        double[] vertices = convertStringToList(road.getVertexes());
        responseDto.getRoads().add(new HistoryResponseDto.Road(vertices));
    }

    return responseDto;
}


private double[] convertStringToList(String vertexesString) {
    String[] vertexStrings = vertexesString.split(" ");
    double vertexArray[] = new double[vertexStrings.length];;
    for (int i = 0; i < vertexStrings.length; i++) {
        vertexArray[i] = Double.parseDouble(vertexStrings[i]);
    }
    return vertexArray;
}

3. Controller

@GetMapping("/route/{routeId}")
public ResponseEntity<HistoryResponseDto> getRoute(@PathVariable Long routeId) {
    return ResponseEntity.ok(historyService.getHistory(routeId));
}

4. html

카카오 맵 API를 불러왔다.
빈 지도를 보여줄 일이 없기에 해당 페이지로 넘어가자마자 이전 기록을 보여주는 함수가 실행되는데,
이는 js에서 구현했다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Kakao Route Search</title>
    <!--  <link rel="stylesheet" href="/css.css">-->
    <script src="https://code.jquery.com/jquery-3.7.0.min.js"
            integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
</head>
<body>

<h1>상세 주행 기록</h1>

<div id="map" style="width:1300px;height:900px;"></div> <!-- 지도가 표시될 영역 -->
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=c87b1c2e1d4bcdc3401e35160e8e7d95"></script>
<!-- 카카오 지도 API -->
<script type="text/javascript"
        src="//dapi.kakao.com/v2/maps/sdk.js?appkey=c87b1c2e1d4bcdc3401e35160e8e7d95&libraries=services,clusterer,drawing"></script>

<script src="/history.js"></script> <!-- 여기에 실제 자바스크립트 파일 경로를 입력하세요 -->

</body>
</html>

5. js

페이지 이동시 바로 실행이 되어야 하기에 .ready를 이용해주었다.

상세정보를 얻기위해 요청을 보내고 받은 data를 카카오 API에서 원하는 형식으로 가공해서 넘겨주면,

상세정보를 확인할 수 있다!

const host = 'http://' + window.location.host;
// 상세 정보 표시
$(document).ready(function() {
    const url = window.location.href;
    const routeId = url.substring(url.lastIndexOf("/") + 1);

    fetch('/api/route/' + routeId)
        .then(response => response.json())
        .then(data => {
            if (!map) {
                map = new kakao.maps.Map(document.getElementById('map'), {
                    level: 3
                });
            }
            let bound = data.bounds;

            var bounds = new kakao.maps.LatLngBounds(
                new kakao.maps.LatLng(bound.min_y, bound.min_x),
                new kakao.maps.LatLng(bound.max_y, bound.max_x)
            );

            map.setBounds(bounds);


            for(let road of data.roads){
                let path = [];
                for(let i=0; i<road.vertexes.length; i+=2){
                    console.log("vertexes: ", road.vertexes[i], road.vertexes[i+1]);
                    path.push(new kakao.maps.LatLng(road.vertexes[i+1], road.vertexes[i]));
                }

                const polyline = new kakao.maps.Polyline({
                    path: path,
                    strokeWeight: 5,
                    strokeColor: '#007bff',
                    strokeOpacity: 0.7,
                    strokeStyle: 'solid'
                });

                polyline.setMap(map);

                polylines.push(polyline);
            }
        })
        .catch(error => {
            console.error('Error:', error);
        });
});


// kakaomap 표시 해주는 곳-----------------------------------------------------------------------------------------------------//
var container = document.getElementById('map');
var options = {
    center: new kakao.maps.LatLng(33.450701, 126.570667),
    level: 3
};

var map = new kakao.maps.Map(container, options);
// 경로 안내 polyline ----------------------------------------------------------------------------------------------------------//
var polylines = [];
function clearPolylines() {
    for (let i = 0; i < polylines.length; i++) {
        polylines[i].setMap(null);
    }
    polylines = [];
}

 

작동 확인

다음처럼 전체 정보와 상세 정보 페이지가 작동하는 것을 확인할 수 있다.

 

구현한 내용들을 보니 Redis를 적용시켜 데이터를 효과적으로 관리할 수 있을 것 같다.

각 과정에 필요 없는 정보들까지 불러오는지 확인이 필요할 것 같다.