지도용 라이브러리로 좌석배치도 만들기

Viola·2020년 5월 27일
0

좌석배치도를 만들게 된 계기

내가 진행했던 프로젝트는 교실 배치도면을 background로 사용하는 좌석배치도 였습니다.

<기존 관리시스템의 단점>

  1. A4용지 안에 학생 이름과 경고나 예외사항등의 항목을 적어 놓고 매 시간마다 학생이 자리에 앉아있는지 확인하는 수기관리 시스템이었는데 이 부분이 넘 불편해 보였어요.

  2. 데이터가 100% 정확할 수 없다는 것을 수기관리의 단점으로 꼽았는데 종이가 사라지거나 물에 젖거나 하면 그 시간의 관리데이터는 사라지는것이나 마찬가지기 때문에 학생이 저는 자리에 앉아있었어요 하면 그냥 인정해줘야 하는것입니다.

<기존 관리시스템 단점을 어떻게 개선할것인가>

  1. 펜으로 체크하는것이 아닌 태블릿이나 스마트폰으로 콕콕콕 눌러가며 쉽게 관리하고 또 담당자가 바뀌더라도 좌석에 앉아있는 학생의 정보를 알 수 있도록 SPA 형태로 만들어 배포합니다.

  2. 로그인 정보를 기준으로 누가 언제 그 교실 공부시간을 관리했는지도 알 수 있게 만듭니다.

n년간 수기로 했던 부분을 바꾼다는것은 누군가에게는 오히려 불편한 부분이 될 수 있다는 생각을 했지만 그런 부분에 대해서는 내부에서 사용하는 것이기 때문에 수시로 피드백을 받아 수정하기로 했습니다.

어떻게 만들었나요

학생 데이터 API는 만들어진 상태였고, 교실이 1-2개가 아니기 때문에 그 많은 데이터를 비교적 빠르게 불러오기 위해 프레임워크를 사용한 SPA 형태를 취했습니다.

1) leaflet.js 는 주로 지도를 불러올 때 사용하는 라이브러리인데, 이미지가 background로 깔려도 괜찮나..?

  • references를 보면 대부분 지도 API URL을 사용하지만 이미지도 상관 없습니다. 경로만 잘 지정하면 됩니다.
  • leaflet.js의 imageUrl 옵션을 프로젝트 내 이미지 경로로 설정하는데 이미지는 SVG로 export하여 라이브러리 안에서 확대가 되어도 깨지지 않도록 했습니다.
  • 옵션에서 background의 Bounds, 즉 범위를 지정해줘야 하는데 leaflet 영역 안에서의 배치도 이미지 사방으로 얼마나 벗어나게 할건지 지정해주는것입니다.

2) 배치도 위 좌석을 어떻게 배치할까?

  • 약 4개의 지점 20개의 교실 500개의 좌석 배치의 방법을 고민하다 한땀한땀 좌석의 좌표를 계산해서 데이터에 반영하는것으로 결정을 했습니다.
  • 각 지점 및 교실마다 좌석 갯수, 간격 모두가 다르고 어떤 지점은 배치도와 상이한 상태로 관리가 되고 있었습니다.
  • 새로운 지점이 오픈되면 건물에 맞추어 기본 배치와는 전혀 다르게 운영되기 때문에 어차피 새로 작업을 해야 합니다.

위와 같은 이유로 좌석배치를 할 때는 열과 행의 간격을 한번씩만 구하고 그 이후부터는 계산을 해서 좌표 데이터에 값을 넣어주었습니다. (노가다 결론..)

// 라이브러리의 클릭 이벤트를 사용하여 좌표 값 알아내기!
map.on('click', function(e) {
          console.log("Lat, Lon : " + e.latlng.lat + ", " + e.latlng.lng)
});
  • 각 교실 컴포넌트를 만들어 페이지 단위로 routing을 하고 background 경로 좌석 좌표 값 지정 범위 데이터는 하나의 js파일로 만들어 모듈처럼 사용했습니다.

middleware/seatInfo.js

export default function () {
	const seatCoordinate = {};
    
    //지점 1
    seatCoordinate.seoulCenter = {
    	room1: {
        	imageUrl: `/assets/images/seoul/room1.svg`,
                coordinate : {
                  1: {x: 797, y: 26},
                  2: {x: 750, y: 26},
                  3: {x: 700, y: 26}, 
                  ...
                }
        }
    },
    
    //지점 2
    seatCoordinate.busanCenter = {
    	room5: {
        	imageUrl: `/assets/images/busan/room5.svg`,
                coordinate : {
                  ...
                  89: {x: 467, y: 330},
                  90: {x: 386, y: 330},
                  91: {x: 305, y: 330},
                }
        }
    }
};
  • 모듈 안에는 각 교실의 데이터도 있지만 localStorage를 사용하여 전체 데이터를 클라이언트의 localStorage에 담도록 되어있습니다.
  if(!localStorage.getItem('seatCoordinate')){
  
  //값이 없어도 일단 지우고 다시 넣어주자.
    localStorage.clear();
    localStorage.setItem('seatCoordinate', JSON.stringify(seatCoordinate));
  }

localStorage를 사용한 이유는 좌표값은 잘 바뀌지 않아 매번 새로 불러올 필요가 없고 돌아다니면서 관리하다 보니 네트워크 상태가 매 분 다르기 때문에 첫 접속시 받아오고 계속 저장해서 사용합니다.

3) 페이지에서 좌석 데이터를 어떻게 불러올까?

  • API에서 좌석 데이터를 호출할 때 요구하는 query string parameter를 컴포넌트에 작성하는데,

  • 해당 교실 컴포넌트가 아니라 layout 폴더 안에 교실 컴포넌트용 roomLayout.vue 를 생성하여 해당 파일에 작성하게 됩니다.

  • roomLayout.vue 에서는 교실 페이지에 들어갔을 때 url parameter를 읽어 지점 이름(query string parameter)을 가져오는 코드를 넣어줍니다.

  • 모듈로 만든 좌석 데이터는 middleware role을 할 수 있도록 roomLayout.vue 에 middleware로 지정합니다.

layout/roomLayout.vue

<script>
export default {
	middleware: ['seatInitialize'],
    
         data(){
              return{
                 classRoom: '',
                 roomNum: ''
              }
          }
</script>
  • 실제 페이지에서 DOM 렌더링을 하기 위해 roomLayout.vue 에서 필요한 action을 dispatch 를 실행합니다.
mounted(){

// url example : https://education.seat.kr/branch/seoul/room1

//url에서 branch 다음으로 / 기준으로 url 잘라 변수에 저장합니다.
const urlPath = $nuxt.$route.path.replace(/^.*?branch\/(.*)/, '$1').split('/'),
		classRoom = urlPath[0],
        roomNum = urlPath[1];

  this.$store.dispatch('getSeatInfo', {
  
  // 모듈에서 아래의 데이터를 가져와서 필요한 action을 disaptch 합니다.
     classRoom: seatInitialize[classRoom][roomNum].floor,
     standard: seatInitialize[classRoom][roomNum].coordinate,
     bounds: seatInitialize[classRoom][roomNum].bounds,     
     imageUrl: seatInitialize[classRoom][roomNum].imageUrl,
   });
 }

store/index.js

  • vuex store안에 라이브러리를 호출하는 이유는 라이브러리를 한번만 선언해 놓고 전역적으로 쓰기 위해서 입니다!
  • 때문에 DOM template 코드도 그 안에 작성합니다.
getSeatInfo({commit, dispatch}, {classRoom, standard, imageUrl, bounds, ...}) {

	/** leaflet.js UI */
	const map = L.map('map', {crs: L.CRS.Simple, maxZoom: 1.2, minZoom : 0.5, doubleClickZoom: false});
	const mapImage = L.imageOverlay(imageUrl, bounds).addTo(map);

	/** leaflet map 그려주세요 */
	map.fitBounds(bounds);
 });

5) 좌석에 앉은 학생 정보 가져오려면?

  • leaflet.js의 bindTooltip 이라는 메소드를 사용하면 좌표 값에 따라 tooltip이 만들어 집니다.


tooltip이라고 하면 대부분 이런것을 떠올리시는데 맞습니다.
<이미지 출처 : https://www.codingfactory.net/10726>

  • 그런데 제가 구현하고 싶은 것은 눌러서 뜨는게 아니라 배치도 위에 올라와 있는 label 같은 UI였습니다.
  • openTooltip 이라는 메소드를 사용하면 tooltip이 열린 상태가 됩니다.
getSeatInfo({commit, dispatch}, {classRoom, standard, imageUrl, bounds, ...}) {
	axios.get(`/api/seat/${classRoom}`)
    	.then(res=> {
        	let arr = res.data.responseValue;
        
         /** 배열 데이터 반복문 */
          for (let i = 0; i < arr.length; i++) {

              // L.circle이라는 Label이 있어야 tooltip 기능을 사용할 수 있고 좌표 지정도 가능합니다.
              let label = L.circle([standard[arr[i].seatNum]['x'], standard[arr[i].seatNum]['y']], {
                  fillColor: 'transparent',
                  stroke: 0,
                  radius: 40
                }).addTo(map).bindTooltip(`
                      <div class="seatWrap">
                          <dl>
                            <dd class="status">
                              <div class="seatNum">${arr[i].seatNum}</div>
                                <div class="${stdGender}">${stdGender}</div>
                              </dd>
                              <dt>${arr[i].student['stdName']}</dt>
                          </dl>
                      </div>
                   `, {
                  permanent: true,
                  direction: 'center',
                  autoPan: false,
                }).openTooltip();
          })
    };
 });
  • 받아온 데이터만큼 반복문을 실행하고 해당 인덱스의 정보를 그려줄 DOM을 bindTooltip 메소드로 구현합니다.

이렇게 하면 leaflet.js를 사용한 작고 귀여운 좌석배치도는 어느정도 구현이 됩니다. 제일 중요한 것은 라이브러리 옵션을 잘 이용해서 원하는대로 최대한 구현하는것입니다. imageUrl L.circle bindTooltip openTooltip 이정도가 구현 시에 필요한 최소 옵션 or 메소드입니다.

끝나지 않는 피드백

프로젝트 베타 버전 배포 후 저와 백엔드 개발자는 무시무시한 피드백에 시달렸지만 피드백 반영을 하면서 또 많이 배우는 계기가 됐습니다.
생각나는 피드백으로는 프로덕트를 사용하고 있는 기기가 다양하고 해상도도 다양해서 "화면에서 전체 배치도를 보고싶어요" 정도가 있는데 boundssetView 옵션을 디테일하게 설정하면 보완이 됩니다.

프로젝트 참고 페이지 : https://engineering.linecorp.com/ko/blog/floor-map-management-system-on-web-with-leaflet/

마지막으로

글을 이렇게 길게 쓴 적이 처음이라 오타나 오류가 있을 수 있습니다,, 부디 발견하시면 댓글을 남겨주시고 도움이 되셨다면 하트 부탁드려요!

profile
글 예쁘게 쓰기

0개의 댓글