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

Random-Drive-Project 기본 MVC 구현(프론트엔드 작업)

by HWK 2023. 10. 26.

완성된 로그인, 길찾기, 길찾기 저장, 네비게이션을 사용자들이 사용하기 편하게 만들어줘야 했다.
이 과정에서 백엔드는 크게 손볼곳이 없었지만, 프론트엔드는 재구성을 해야했다.
팀 회의를 거쳐 만들어진 와이어프레임은 다음과 같다.

나는 이중에서 navigation과 history를 맡게되었고 해야할 작업은 아래와 같았다.

  1. navigation으로 넘어오는 정보들 구분하기
  2. 반경기반 길찾기 저장시 목적지 정보 저장
  3. navigaion에서 navigationGuide로 필요한 데이터 넘겨주기
  4. navigation과 history 모바일 규격에 맞춰 디자인 수정

navigation으로 넘어오는 정보들 구분하기

1. 어떤 기능의 길찾기 정보인지 저장시 넘겨주기

function saveRoute(data, originAddress, destinationAddress) {

    const auth = getToken();
    var decodedOriginAddress = decodeURIComponent(originAddress);
    var decodedDestinationAddress = decodeURIComponent(destinationAddress);

    fetch('/api/routes', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': auth // 인증 토큰을 Authorization 헤더에 추가
        },
        body: JSON.stringify({
            requestData: data, // KakaoRouteAllResponseDto 객체
            originAddress: decodedOriginAddress,
            destinationAddress: decodedDestinationAddress,
            mapType: type
        })
    })
}

navi.js의 한 함수이다. navigation 진입 단계인 search에서 해당 길찾기 기능이 어떤 기능인지 넘겨줄 것이다.

그 정보를  type에 담아서 반환해 줄 것이다.

2. 넘겨준 데이터 저장하기

AllHistoryResponseDto에 mapType이라는 요소를 추가한다.

@JsonProperty("mapType")
private String mapType;

해당 responseDto를 잘 받을 수 있도록 Route Entity 클래스에도 mapType를 추가해준다.

@Column(name = "mapType", nullable = false)
private String mapType;

3. 길찾기 API 요청시 mapType 바로바로 구분해주기

HomeController에 내가 원하는 요청 타입을 만들것이고, 이 타입에 맟줘 search를 구현하는 팀원과 history를 구현하는 내가 요청을 보내준다. @PathVariable로 한 이유는 비록 하나의 코드는 조금 더러워 보일 수 있지만, 이 코드 하나가 있으므로, requestDto를 따로 만들지 않아도 된다! 심지어 사이트를 실행시킬 때 해당 데이터가 잘 넘어오는지 바로바로 확인이 가능하다. 또한 굳이 보호해줄 필요가 없는 데이터셋이기 때문에 @PathVariable를 채택했다.

@GetMapping("/navi/{type}/{routeId}/{originAddress}/{destinationAddress}/{redius}")
@Operation(summary = "네비게이션", description = "네비게이션 화면을 출력합니다.")
public String showNavi(@PathVariable Long routeId,@PathVariable String type,
                       @PathVariable String originAddress,@PathVariable String destinationAddress,
                       @PathVariable int redius, Model model) {
    model.addAttribute("routeId", routeId);
    model.addAttribute("type", type);
    model.addAttribute("originAddress", originAddress);
    model.addAttribute("destinationAddress", destinationAddress);
    model.addAttribute("redius", redius);
    return "navi";
}

어쨋든 저 type에 mapType를 넣어서 보내줄 것이다.
navi.js에서는 다음과 같이 담아준 정보를 이용한다.

const segments = url.split("/");
const type = segments[segments.length - 5]; // 뒤에서 다섯 번째 segment
const routeId = segments[segments.length - 4]; // 뒤에서 네 번째 segment
const originAddress = segments[segments.length - 3]; // 뒤에서 세 번째 segment
const destinationAddress = segments[segments.length - 2]; // 뒤에서 두 번째 segment
const redius = segments[segments.length - 1]; // 마지막 segment
$(document).ready(function() {
    if (type === 'save') {
        makeHistoryMap(routeId);
    } else if (type === 'live') {
        makeNavi(originAddress, destinationAddress);
    } else if (type === 'live-random') {
        makeRandomNavi(originAddress, destinationAddress, redius);
    } else if (type === 'live-all-random') {
        makeAllRandomNavi(originAddress, redius);
    } else {
        return;
    }
});
// 예 버튼
document.getElementsByClassName('button-yes')[0].addEventListener('click', function() {
    if (type === 'live') {
        saveRoute(responseData, originAddress, destinationAddress)
    } else if (type === 'live-random') {
        saveRoute(responseData, originAddress, destinationAddress)
    } else if (type === 'live-all-random') {
        saveRoute(responseData, originAddress, '무작위 주소')
    } else {
        return;
    }
});

위 코드를 보면 주소에 할당된 정보를 가져와서, 각각의 type에 맞게 서로 다른 기능을 실행하는 것을 볼 수 있다. 

LiveMap과 HistoryMap은 각각 넘어오는 데이터 형식이 다르니 주의하자.
해당 기능에 대한 설명은 아래 블로그에 작성되어 있다.
https://hwk99.tistory.com/118

 

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

경로 저장이 완료되었으니 경로 전체 조회, 상세 조회를 구현해야 한다. 경로 저장에 대한 설명은 아래 링크에 있다. https://hwk99.tistory.com/117 53일차 - Random-Drive-Project 기본 MVC 구현(경로 저장) 경로

hwk99.tistory.com

 

반경기반 길찾기 정보 저장시 목적지도 반환해주기

1. 기존 문제점

기존에 반경 기반 데이터를 저장할 때 다음과 같은 형식으로 넘겨주었다.

function saveAllRandomRoute(data, originAddress) {

    const auth = getToken();

    fetch('/api/routes', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': auth // 인증 토큰을 Authorization 헤더에 추가
        },
        body: JSON.stringify({
            requestData: data, // KakaoRouteAllResponseDto 객체
            originAddress: originAddress,
            destinationAddress: '무작위 주소', // destinationAddress 
            mapType: type
        })
    })
}

이유는 다른 기능과 다르게 목적지를 직접 입력하지 않아서 destinationAddress이 생성되지 않기 때문이다.

2. 문제 해결 방법 구상

먼저 랜덤으로 목적지를 가져올때 그 정보를 가로채서 responseDto에 직접 넣어주고자 했다.
하지만 해당 방법은 사용할 수 없었다. RandomKakaoRouteSearchService에서 다음 함수가 실행될 시 return 값에 목적지와 출발지의 주소명은 저장되지 않았기 때문이다.
kakao API를 사용하는 것이기 때문에 저 함수에서는 어떻게 할 방법이 떠오르질 않았다.

private KakaoRouteAllResponseDto makeRequestForm(DocumentDto origin, DocumentDto destination, DocumentDto waypoints){
    Map<String,Object> uriData = new TreeMap<>();
    uriData.put("origin",new DocumentDto(origin.getAddressName(),origin.getLatitude(), origin.getLongitude()));
    uriData.put("destination",new DocumentDto(destination.getAddressName(),destination.getLatitude(),destination.getLongitude()));

    uriData.put("waypoints",new ArrayList<>(Arrays.asList(
            new DocumentDto(waypoints.getAddressName(), waypoints.getLatitude(), waypoints.getLongitude())
    )));
    uriData.put("priority","RECOMMEND");

    URI uri = URI.create("https://apis-navi.kakaomobility.com/v1/waypoints/directions");

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
    HttpEntity <Map<String,Object>> httpEntity = new HttpEntity<>(uriData,headers);

    return restTemplate.postForEntity(uri,httpEntity,KakaoRouteAllResponseDto.class).getBody();
}

3. 문제 해결

결국 새로운 Entity와 Repository를 만들어 목적지만 따로 저장하는 방법을 하기로 했다.
이 방법을 기피한 이유는 Database 접근 횟수를 증가시킬 것이기 때문이다.

아래는 새로 만든 클래스들이다.

@Entity
@Getter
@NoArgsConstructor
@Table(name = "randomDestination")
public class RandomDestination {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username", nullable = false)
    private String username;

    @Column(name = "destinationAddress", nullable = false)
    private String destinationAddress;

    public RandomDestination(String username, String destinationAddress) {
        this.username = username;
        this.destinationAddress = destinationAddress;
    }
}
public interface RandomDestinationRepository extends JpaRepository<RandomDestination, Long> {
    RandomDestination findByUsername(String username);
}

크기가 크지 않지만, 너무 많은 값이 저장되는걸 막고자 한사람당 하나의 RandomDestination 데이터를 가질 수 있게 설계했다. 만일 사용자가 이미 목적지를 저장해놓았다면 해당 데이터를 삭제하고 다시 저장하는 식이다.
흠.. 하지만 이제와 생각하니 update로 수정하면 db 접근 횟수를 줄일 수 있을 것 같다.
아래는 RandomKakaoRouteSearchService에 해당 기능을 추가해놓은 메서드이다.

public KakaoRouteAllResponseDto requestAllRandomWay(String username, String originAddress, Integer redius) {

   if (ObjectUtils.isEmpty(originAddress) || ObjectUtils.isEmpty(redius)) return null;

    // 출발지와 도착지 주소를 각각 좌표로 변환
    DocumentDto origin = kakaoAddressSearchService.requestAddressSearch(originAddress).getDocumentDtoList().get(0);

    /***
     목적지와 경유지 값을 반경으로 계산해서 가져오는 메소드
     ***/
    KakaoApiResponseDto responses = kakaoCategorySearchService.requestPharmacyCategorySearch(origin.getLatitude(), origin.getLongitude(), redius);

    /***
     랜덤으로 다중 목적지와 경유지 만들기 알고리즘
     ***/
    int RandomLength = responses.getDocumentDtoList().size();

    Random rd = new Random();

    int destinationCnt = rd.nextInt(RandomLength);
    int waypointsCnt = rd.nextInt(RandomLength);

    if(destinationCnt == waypointsCnt){
        waypointsCnt = destinationCnt - 1;
    }

    DocumentDto destination = responses.getDocumentDtoList().get(destinationCnt);
    DocumentDto waypoints = responses.getDocumentDtoList().get(waypointsCnt);

    // 목적지 DB에 남김, 만일 동일 사용자가 이미 목적지를 저장해 놓았다면, 삭제
    RandomDestination randomDestination = new RandomDestination(username, destination.getAddressName());
    RandomDestination olderRandomDestination = randomDestinationRepository.findByUsername(username);
    if (olderRandomDestination != null)
        randomDestinationRepository.delete(olderRandomDestination);
    randomDestinationRepository.save(randomDestination);

    /***
     요청 헤더 만드는 공식
     ***/

    return makeRequestForm(origin,destination,waypoints);
}

해당 정보를 저장해 놓았기 때문에 History에 해당 경로를 저장할때, HistoryService의 saveHistory 메서드에서
다음과 같은 코드를 추가하면 반경 기반 랜덤 길찾기에서도 목적지를 저장할 수 있다.

if (mapType.equals("live-all-random")) {
    RandomDestination olderRandomDestination = randomDestinationRepository.findByUsername(user.getUsername());
    destinationAddress = olderRandomDestination.getDestinationAddress();
}

 

navigation에서 navigation guide로 정보 넘겨주기

1. Live data 넘겨주기

이 부분은 navigation guide를 개발한 팀원과 조율했다.
그 결과 와이어프래임과 같이 페이지를 나누지 않고, navigation내부에서 guide를 구현하기로 했다.
하지만, 서로 소통이 부족했던 탓에, history에 대한 guide는 아직 구현하지 못했다. 
고로 Live data를 넘겨줄 필요도 없었고, guide 기능에서는 이미 만들어진 Data를 가져다 쓰면 된다.
아래 코드는 guide 기능의 시작이다. 

function onClick_StartNavi_navi()
{
    pathData = responseData;
    
    getNextGuidPoint(false);
    getGuidPoint(true);
    startCorutine();

    //guid_info
    if(!document.getElementById("input_StartNavi").classList.contains("disabled"))
        document.getElementById("input_StartNavi").classList.add("disabled");
    if(document.getElementById("guid_info").classList.contains("disabled"))
        document.getElementById("guid_info").classList.remove("disabled");

    if (pathData != null) {
        let startPoint = pathData.routes[0].sections[0].guides[0];
        update(startPoint.y, startPoint.x);

        map.setLevel(3, {animate: true});// 사용시 보이는 위치 달라짐
        panTo(startPoint.y, startPoint.x);
    }
}

 

navigation과 history 디자인 모바일 규격에 맞춰주기

1. navigation 디자인

아래와 같은 디자인으로 완성되었다.

demo-container가 주변의 푸른 태두리를 만들어주며, phone screen이 길찾기 정보를 표시하는 위치이다.

.demo-container {
    width: 100vw;
    min-height: 100vh;
    background: radial-gradient(farthest-corner at 50% 50%, #6270a4, #3B4465);
    font-family: "Oxygen Mono", sans-serif;
    overflow: hidden;
}

.phone-back, .demo-container {
    display: flex;
    align-items: center;
    justify-content: center;
}


.phone-screen {
    width: 18rem;
    height: 32rem;
    background: #ffffff;
    overflow: hidden;
    flex-shrink: 0;
    position: relative;
    display: flex;
    flex-direction: column;
}

다음 코드들은 phone-screen 내부의 버튼과 안내판을 꾸며준다.
두 button을 담고있는 button-row에는 justify-content: space-between; 이라는 코드를 추가해 버튼을 양 옆으로 배치했다.

또한 .button:hover에서는 버튼에 포인터를 가져다 놓았을 시 색이 변하도록 해주는 코드이다.

.distance-duration {
    flex: 1;
    margin: 0.5rem;
    background: #3949d5;
    color: white;
    bottom: 3rem; /* 예/아니오 버튼 위에 위치하도록 조정 */
    text-align: center;
    font-family: "SDSamliphopangche_Basic", sans-serif;
    font-size: 1rem;
    line-height: 3rem; /* 버튼의 행 높이와 일치하도록 설정 */
    border-radius: 2rem;
    padding: 0 1rem; /* 텍스트 내용에 대한 좌우 패딩을 추가합니다. */
    align-items: center; /* 수직 가운데 정렬을 위해 추가 */
    justify-content: center; /* 수평 가운데 정렬을 위해 추가 */
}
.button {
    flex: 1;
    margin: 0.5rem;
    background: #3949d5;
    color: white;
    font-family: "SDSamliphopangche_Basic", sans-serif;
    font-size: 1rem;
    text-align: center;
    line-height: 3rem; /* 버튼의 행 높이와 일치하도록 설정 */
    cursor: pointer;
    border-radius: 2rem;
    transition: background-color 0.3s;
    padding: 0 1rem; /* 텍스트 내용에 대한 좌우 패딩을 추가합니다. */
    align-items: center; /* 수직 가운데 정렬을 위해 추가 */
    justify-content: center; /* 수평 가운데 정렬을 위해 추가 */
}
.button-row {
    display: flex;
    justify-content: space-between;
    height: 3rem; /* 버튼 행의 높이를 조정합니다. */
    width: 100%; /* 부모 요소에 꽉 차게 설정 */
}
.button:hover {
    background-color: #148ef1; /* 마우스를 가져다 댔을 때 배경색을 흰색으로 변경 */
    color: #ffffff; /* 마우스를 가져다 댔을 때 텍스트 색상을 원래 색으로 변경 */
    /*box-shadow: 0 0 10px #181f57; !* 선택 효과(섀도우) 추가 *!*/
}

2. history 디자인

아래와 같은 디자인으로 완성되었다.

로그아웃 버튼 고정은 다른 팀원이 맡기로 했고, 주목해야할 코드는 아래와 같다.

/* 랜덤 또는 랜덤이 아니면 색상을 변경 */
#historyTableBody tr[data-maptype="live"] {
    background-color: #F0F8FD; /* 푸른색 배경 #3498db */
    color: #023656; /* 텍스트 색상을 흰색으로 설정 */
}

#historyTableBody tr:not([data-maptype="live"]) {
    background-color: #FEF0F6; /* 붉은색 배경 #e74c3c */
    color: #57002A; /* 텍스트 색상을 흰색으로 설정 */
}

#historyTableBody tr:hover {
    background-color: #8bcbf2; /* 마우스를 올렸을 때의 배경색을 지정 forestgreen */
}

data maptype가 live면 일반 길찾기고 그 외는 랜덤 길찾기이다.
사용자가 한눈에 구분을 할 수 있게 하고 싶었고, 문구를 넣자니 핸드폰 규격에 맞지 않는 결과가 나올 것 같았다.
붉은색상은 랜덤 길찾기를 나타내고, 푸른색상은 일반 길찾기를 나타낸다.
이 또한 hover을 이용해 마우스를 올리면 색이 변하도록 설정했다.

 

이로써 맡은 부분의 프론트를 모두 해결하고, 코드를 병합할 수 있었다.
기본 기능이 완성 되었으니, 이젠 기능 발전을 시켜보자.