본문 바로가기
Develop/JSP

구글 지도 API 써서 일정 페이지 만들기

by ys2ys2 2024. 10. 22.

팀 프로젝트 완성도를 채우기 위해 촉박한 일정이지만 새로 페이지를 맡았다!

 

google map API로 여행 일정 페이지를 만드는 작업이였다.. 일단은 완성했으니 공부겸 정리하기!

 

일단..

완성된 페이지

jsp로 작업했고 자바스크립트를 70% 넘게 쓴거 같다.

 

tripSched.jsp

 


 

구성

 

placeholder 걸어놓은 제목을 입력해주세요 부분에 사용자가 입력할 수 있게 했다. (여행 일정 제목)

그 밑에 달력으로 날짜를 받게 했는데 달력은

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />

 

위 코드의 외부 css를 사용했다! 깔끔하고 쓰기 편한거 같다. 버튼 색깔 css정도만 해줬다.

daterangepoicker

 

그 밑으로는 google map API를 써서 지도를 넣었다. (마커와 선으로 경로 표시 예정)

 

google map API

 

 

지도 밑에는 DAY라는 카드를 만들어서 정렬했고 기본적으로 2개를 놨다. 우측에 일정 추가 버튼으로 DAY3 DAY4 쭉쭉 만들 수 있게 했다.

 

일정 카드 부분
카드 추가

 

 

각 카드별로 일정 추가를 누르면 우측에 도시와 장소를 검색할 수 있게 했고 편의성을 위해 배경을 어둡게 처리했다.

 

도시, 여행지 검색

 

 

자동완성을 넣었고 도시를 선택하면 반경 50km 의 장소들이 리스트로 불러와지게 했다. (반경은 수정 가능) 

그리고 리스트에는 여행지 이름과 주소를 표시하게 했다.

 

자동완성
여행지 선택1

 

그 후 선택버튼을 누르면 선택한 여행지가 태그로 들어가고

google map에 마커가 찍히면서 숫자가 들어가면서 선으로 표시가 되게 했다.

여행지 선택2
여행지 선택3

 

저장을 누르면 각 카드에 리스트 형식으로 표현되게 했다.

day 카드에 저장

 

각각 DAY 카드를 선택하면 기존 일정이 없을땐 빈 화면으로, 저장된 일정이 있으면 저장된 일정이 나오게 해서

각 DAY 마다 여행 일정들을 자유롭게 수정할 수 있게 했다.

 

이 부분까지 사용한 것들

google map api

google.maps.Polyline 경로 그리기

google.maps.places.PlacesService 지도에 장소 검색

google.maps.places.Autocomplete 자동완성

service.textSearch 텍스트로 장소 검색

google.maps.Marker 지도 마커

service.nearbySearch 특정 지역 내 근처 장소 검색

 

구글에서 제공되는 것들이 다양하고 사용하기도 쉬워서 script를 추가해서 사용했다.

여행지 선택한 순서대로 마커가 찍히면서 숫자와 함께 경로가 추가되게 했고

자동완성을 통해 사용자들이 편하게 입력할 수 있게 했다.

특정 지역 내 근처 장소 검색을 통해 도시만 검색해도 추천 여행지처럼 리스트로 보여지게 했다.

 

문제는 이 값들을 어떻게 DB에 넣고 어떤 방식으로 넣을지가 고민이였다.. 나중에 저장된 DB값으로

마이페이지에서 불러오거나, 새로운 페이지에서 내가 짠 여행 일정들을 확인해야 했기 때문에

초기부터 잘 생각해서 짜야 했다.

 

일단 필요한 값들은

m_idx (마이페이지에서 조인할 때 사용)

post_id (저장 버튼 한번 누르면 같은 번호로 저장되기. 마이페이지에서 사용)

m_email (세션에서 받아오기, 로그인 한 사용자 or email 비교할때 사용)

m_nickname (세션에서 받아오기)

period_start (여행 시작 날짜)

period_end (여행 끝 날짜)

title (여행기 제목)

day_number (day 옆 숫자)

city_name (도시 이름)

label_number (라벨 순서 관리용도)

place_name (여행지 장소)

place_address (여행지 주소)

 

이렇게 생각하고 만들었다.

 

로그인 한 상태에서 세션에 저장된 m_email이나 m_nickname을 통해서 추후에 마이페이지나 다른 페이지에서 사용할 수 있게 했다.

 

post_id는 각 저장 버튼 한번에 같은 번호들이 순차로 생성되게 해서 마이페이지에서 관리할 수 있게 했다.

(ex_여행기 제목 : 제주도 여행, day1 day2 day3 여행일정 짜고 저장 누르면 day1 day2 day3에 해당하는 post_id는 전부 같다.)

 

period_start,end는 달력에서 구분지어서 저장하게 했다.

달력에 - 를 기준으로 앞은 start 끝은 end로 나뉘게 했다. (여행기 저장 페이지에서 가져와서 표시할 예정) 

 

title은 사용자한테 입력을 받아서 여행 일정 제목으로 저장되게 했다.

 

day_number는 day 옆 숫자로 추후에 여행기 저장 페이지에서 순서대로 배열해서 출력하려고 넣었다.

city_name은 마이페이지에서 도시 이름으로 가져올 수 있게 했다.

label_number은 추후에 여행기 저장 페이지에서 저장된 순서로 여행 순서, 라벨, 선을 다시 찍히게 하려고 했다.

place_name은 마이페이지에서 여행지 장소로 가져올 수 있게 했다.

place_address도 마찬가지로 마이페이지에서 사용 예정.

 


 

 

생긴 오류, 해결 방법

 

1. innerHTML 문제

 

이걸 만들면서 큰 오류를 3가지정도 접했다..

 

첫 오류는 innerHTML을 써서 값을 백엔드로 전달하려고 했던 점..

innerHTML

 

innerHTML이란?

innerHTML은 자바스크립트에서 HTML 요소의 내용을 동적으로 변경하거나 접근하는 데 사용되는 속성입니다.

이 속성은 선택된 HTML 요소의 내부 콘텐츠(HTML 태그와 텍스트)를 읽거나 수정할 수 있습니다.


 

각 day 카드가 있어서 DB에 저장해야 할 값들을 배열로 주려고 했다(day가 여러개면 여러개를 받아야 하니까)

period_start, period_end, city_names[], days[] 등이 하나도 안 들어가고 빈 배열로만 들어가고 있었다..

문제는 전달 방식을 <input> 필드에 있는 innerHTML을 써서 동적으로 추가했었는데 이게 문제였다.

 

처음에 innerHTML을 썼던 건 동적 요소들을 쉽게 추가하고 처리할 수 있다고 생각해서 그렇게 했다.

단일 문자열로 여러 HTML 요소들을 한번에 추가할 수 있어서 빠르고 간단할거 같았다.

 

하지만 day 카드들이 여러개고 innerHTML을 설정할 때마다 기존 DOM 구조가 초기화되고

새로운 구조로 덮어져서 데이터가 자꾸 날아갔다.

 

그래서 어떻게 값을 전달해야 할까 고민하다가 createElement랑 setAttribute로 바꿔서 해결했다.

 

createElement랑 setAttribute를 써서 각각의 필드를 하나씩 추가하고, 각 필드에 들어가는 값들을 쉽게 처리했다.

기존에 innerHTML을 쓸 땐 DOM이 모두 제거되면 새로 작성된 HTML로 덮이기 때문에 값들이 자꾸 날아갔지만

createElement로 기존의 DOM을 유지한 채 새로운 요소만 추가하면서 값들이 컨트롤러로 잘 넘어가게 됐다.

 

//일정 저장하기 전에 각 day의 정보를 hidden input으로 추가
function prepareScheduleData() {
    const title = document.getElementById('titleInput').value;
    const dateRange = document.getElementById('dateInput').value.split(' - ');

    const period_start = dateRange[0];
    const period_end = dateRange[1];

    const hiddenFieldsContainer = document.getElementById('hiddenFieldsContainer');
    hiddenFieldsContainer.innerHTML = ''; // 이전 데이터를 초기화

    // period_start와 period_end를 hidden 필드로 추가
    const periodStartInput = document.createElement('input');
    periodStartInput.setAttribute('type', 'hidden');
    periodStartInput.setAttribute('name', 'period_start');
    periodStartInput.setAttribute('value', period_start);


    const periodEndInput = document.createElement('input');
    periodEndInput.setAttribute('type', 'hidden');
    periodEndInput.setAttribute('name', 'period_end');
    periodEndInput.setAttribute('value', period_end);

    // hiddenFieldsContainer에 추가
    hiddenFieldsContainer.appendChild(periodStartInput);
    hiddenFieldsContainer.appendChild(periodEndInput);


    // 도시 이름을 hidden 필드로 추가 (현재 선택된 도시 이름)
    if (currentCityName) {
        const cityNameInput = document.createElement('input');
        cityNameInput.setAttribute('type', 'hidden');
        cityNameInput.setAttribute('name', 'city_names');
        cityNameInput.setAttribute('value', currentCityName);

        hiddenFieldsContainer.appendChild(cityNameInput);
    }

    // selectedPlacesPerDay 객체에 있는 데이터를 hidden 필드로 추가
    Object.keys(selectedPlacesPerDay).forEach((day, index) => {
        const places = selectedPlacesPerDay[day];

        if (places && places.length > 0) {
            places.forEach((place, placeIndex) => {
                const dayNumber = day.replace('day', '');  // 'day1' -> '1'로 변환

            	
                // 각 값들을 확인
                console.log("Day:", day);
                console.log("City Name:", currentCityName);
                console.log("Place Name:", place.name);
                console.log("Place Address:", place.vicinity);
                console.log("Place Index:", placeIndex + 1);

                // day 입력 필드 생성
                const dayInput = document.createElement('input');
                dayInput.setAttribute('type', 'hidden');
                dayInput.setAttribute('name', 'days[]');
                dayInput.setAttribute('value', dayNumber);  // 변환된 숫자 값 사용

                // city_names 입력 필드 생성
                const cityInput = document.createElement('input');
                cityInput.setAttribute('type', 'hidden');
                cityInput.setAttribute('name', 'city_names[]');
                cityInput.setAttribute('value', currentCityName);

                // label_numbers 입력 필드 생성
                const labelInput = document.createElement('input');
                labelInput.setAttribute('type', 'hidden');
                labelInput.setAttribute('name', 'label_numbers[]');
                labelInput.setAttribute('value', placeIndex + 1);

                // place_names 입력 필드 생성
                const placeNameInput = document.createElement('input');
                placeNameInput.setAttribute('type', 'hidden');
                placeNameInput.setAttribute('name', 'place_names[]');
                placeNameInput.setAttribute('value', place.name);

                // place_addresses 입력 필드 생성
                const placeAddressInput = document.createElement('input');
                placeAddressInput.setAttribute('type', 'hidden');
                placeAddressInput.setAttribute('name', 'place_addresses[]');
                placeAddressInput.setAttribute('value', place.vicinity);

                // hiddenFieldsContainer에 추가
                hiddenFieldsContainer.appendChild(dayInput);
                hiddenFieldsContainer.appendChild(cityInput);
                hiddenFieldsContainer.appendChild(labelInput);
                hiddenFieldsContainer.appendChild(placeNameInput);
                hiddenFieldsContainer.appendChild(placeAddressInput);
            });
        }
    });

 

 


2. 여행 일정에 동일한 post_id 부여하기

 

 

사실 생각치도 못하고 있다가 다 만들어놓고 테스트 하면서 수정했다..

저장한 게시글을 나중에 불러와야 하는데 이게 언제 만든 여행 일정인지 알 수가 없었다.

그래서 post_id를 줘서 저장 버튼을 한번 누르면 같은 post_id값을 줘서 나중에 마이페이지나, 상세보기 페이지에서

같은 post_id를 가진 day_card에 해당 값들을 출력하게 하려고 했다.

 

title로 하려고도 생각해봤는데 제목이 중복으로 되면 더 꼬일거 같아서 post_id를 넣었다.

 

구현체인 Impl에서 DAO→MAPPER 로 넘기고 post_id의 최대값을 조회한 후 저장버튼 누를 때 마다 +1로 저장되게 했다.

 

ServiceImpl

@Service
public class TripSchedServiceImpl implements TripSchedService {

    @Autowired
    private TripSchedDAO tripSchedDAO;

    @Override
    public void saveTripSchedule(TripSchedVO tripSchedVO) {
    	
    	// 현재 저장된 post_id 중 가장 큰 값 찾기
        Integer maxPostId = tripSchedDAO.getMaxPostId();  // DAO에서 post_id 최대값 조회
        int newPostId = (maxPostId != null) ? maxPostId + 1 : 1; // 새로운 post_id 설정
    	
    	
    	
        // 각 DAY의 장소 정보를 반복문을 통해 저장 + 같은 post_id 가지기
        for (int i = 0; i < tripSchedVO.getDayNumbers().length; i++) {
            TripSchedVO vo = new TripSchedVO();
            vo.setM_idx(tripSchedVO.getM_idx());
            vo.setM_email(tripSchedVO.getM_email());
            vo.setM_nickname(tripSchedVO.getM_nickname());
            vo.setTitle(tripSchedVO.getTitle());
            vo.setPeriod_start(tripSchedVO.getPeriod_start());
            vo.setPeriod_end(tripSchedVO.getPeriod_end());
            
            vo.setPost_id(newPostId);  // 같은 post_id를 설정


            // 배열에서 해당 인덱스의 데이터들을 설정
            vo.setDay_number(tripSchedVO.getDayNumbers()[i]);
            vo.setCity_name(tripSchedVO.getCityNames()[i]);
            vo.setLabel_number(tripSchedVO.getLabelNumbers()[i]);
            vo.setPlace_name(tripSchedVO.getPlaceNames()[i]);
            vo.setPlace_address(tripSchedVO.getPlaceAddresses()[i]);

            // DAO에 데이터 저장
            tripSchedDAO.insertTripSchedule(vo);
        }
    }
    
    
    
}

 

DAO

@Repository
public class TripSchedDAO {

  //현재 가장 큰 post_id 조회
    public Integer getMaxPostId() {
    	return sqlSession.selectOne(MAPPER + ".getMaxPostId");
        
    }
}

 

MAPPER

<MAPPER>

	<!-- 가장 큰 post_id 조회 -->
    <select id="getMaxPostId" resultType="Integer">
        SELECT MAX(post_id) FROM travel_schedule
    </select>
    
</MAPPER>

 

이렇게 해서 저장 한번에 같은 post_id를 갖게 했다.

DB

 

 


 

 

3. 배열 값 처리할때 index sync 문제

 

보통 저장하기 한번 누르면 하나의 행에 값들이 들어가게 했었는데

지금은 day 카드들마다 각각 다른 값들도 저장해야 하기 때문에 배열로 값들을 받아서 처리해야 했었다.

 

배열로 전송된 데이터들이 순서가 다르게 처리되면 dayNumbers[], cityNames[], placeNames[] 등의 필드값들의 데이터가 불일치하게 들어갈 수 있는 문제가 있었다.

 

문제의 코드

for (int i = 0; i < tripSchedVO.getDayNumbers().length; i++) {
    TripSchedVO vo = new TripSchedVO();
    vo.setM_idx(tripSchedVO.getM_idx());
    vo.setM_email(tripSchedVO.getM_email());
    vo.setM_nickname(tripSchedVO.getM_nickname());
    vo.setTitle(tripSchedVO.getTitle());
    vo.setPeriod_start(tripSchedVO.getPeriod_start());
    vo.setPeriod_end(tripSchedVO.getPeriod_end());

    // 배열에서 해당 인덱스의 데이터들을 설정 (인덱스 일치 문제 발생)
    vo.setDay_number(tripSchedVO.getDayNumbers()[i]);
    vo.setCity_name(tripSchedVO.getCityNames()[i]);
    vo.setLabel_number(tripSchedVO.getLabelNumbers()[i]);
    vo.setPlace_name(tripSchedVO.getPlaceNames()[i]);
    vo.setPlace_address(tripSchedVO.getPlaceAddresses()[i]);

    tripSchedDAO.insertTripSchedule(vo);
}

 

 

그래서 for문을 사용해서 각 배열의 인덱스를 동기화해서 처리했다.

 

해결한 코드

for (int i = 0; i < tripSchedVO.getDayNumbers().length; i++) {
    TripSchedVO vo = new TripSchedVO();
    vo.setM_idx(tripSchedVO.getM_idx());
    vo.setM_email(tripSchedVO.getM_email());
    vo.setM_nickname(tripSchedVO.getM_nickname());
    vo.setTitle(tripSchedVO.getTitle());
    vo.setPeriod_start(tripSchedVO.getPeriod_start());
    vo.setPeriod_end(tripSchedVO.getPeriod_end());

    vo.setPost_id(newPostId);  // post_id도 추가 설정

    // 배열에서 각 필드의 데이터들을 정확한 인덱스로 설정
    vo.setDay_number(tripSchedVO.getDayNumbers()[i]);
    vo.setCity_name(tripSchedVO.getCityNames()[i]);
    vo.setLabel_number(tripSchedVO.getLabelNumbers()[i]);
    vo.setPlace_name(tripSchedVO.getPlaceNames()[i]);
    vo.setPlace_address(tripSchedVO.getPlaceAddresses()[i]);

    // DAO에 데이터 저장
    tripSchedDAO.insertTripSchedule(vo);
}

 

 


 

 

이렇게 값이 DB에 잘 찍히는거 보고 바로 가방싸서 집에 왔다.

 

요렇게 정리 끝.. 이 페이지에서 또 생기는 문제들은 여기다가 더 수정할 예정!

 

자야지..