프로그래밍/Vue.js

vuejs 공공데이터 공적 마스크 판매 정보 api 사용해보기 (3)

p-a-r-k 2020. 3. 13. 18:05
반응형

오늘의 목표...

 

이전에는 공공데이터 api를 사용해서 리스트를 출력하고,

 

vuejs 공공데이터 공적 마스크 판매 정보 api 사용해보기 (2)

이전에는 공공데이터 api를 이용해서 간단하게 ui구현을 해보았다. /storesByAddr api를 이용해서 address값을 조회조건으로 추가해보려고한다. 일단 행정구역은 api를 찾기 귀찮(?)아서 통계분류포털 에서 행정구..

19park.tistory.com

행정구역 분류를 mysql로 관리하여 nodejs express로 대충 api를 만들어서 조회조건을 구현했다.

이제 리스트를 클릭하면 판매처 상세정보와 위/경도값을 가지고 지도에 표시해보려한다..

 

일단 구현할 상세페이지 컴포넌트를 만든다.

vuetify의 bottom sheet로 한번 만들어보자. 그리고 doOpen과 doClose 메서드가 있다.

doOpen메서드는 sheet가 열리면서 전달받은 item을 바인딩 해줄것이다.

기본적으로 name과 stock_at, addr을 보여주도록 하겠다.

// src/pages/Detail.vue
<template>
    <v-bottom-sheet v-model="sheet">
        <v-sheet>
            <v-card
                class="mx-auto"
            >
                <v-list-item>
                    <v-list-item-avatar :color="$parent.getStatColor(item.remain_stat) || 'grey'"></v-list-item-avatar>
                    <v-list-item-content>
                        <v-list-item-title class="headline">{{item.name}}</v-list-item-title>
                        <v-list-item-subtitle>입고시간: {{item.stock_at}}</v-list-item-subtitle>
                    </v-list-item-content>
                    <v-list-item-action>
                        <v-btn icon
                               :large="true"
                               @click="doClose">
                            <v-icon color="red">clear</v-icon>
                        </v-btn>
                    </v-list-item-action>
                </v-list-item>

                <v-card-text class="pb-0">
                    <h3 style="color: #fff;">{{`재고 - ${$parent.getStatCount(item.remain_stat)}개 이상`}}</h3>
                </v-card-text>
                <v-card-text>{{item.addr}}</v-card-text>
            </v-card>
        </v-sheet>
    </v-bottom-sheet>
</template>

<script>
    export default {
        name: "Detail",
        data() {
            return {
                sheet: false,
                item: {
                    name: null,
                    addr: null,
                    remain_stat: null,
                    lat: null,
                    lng: null
                }
            };
        },
        methods: {
            doOpen(item) {
                this.sheet = true;
                this.item = item;
            },
            doClose() {
                this.sheet = false;
            }
        }
    };
</script>

<style scoped>

</style>

Main.vue에서는 Detail.vue를 import해주고 component등록해주고,

<template/> 쪽에 컴포넌트를 둔다. ref속성도 주자.

v-list를 사용하여 구현한 판매처 리스트 쪽에는 @click이벤트로 doOpenDetail(item) 를 호출하도록 했다.

// Main.vue tempalte
<template>
    <v-container fluid>
        ...
        <v-list three-line>
            <v-subheader>마스크 판매처 정보</v-subheader>
            <v-list-item v-if="!list.items.length">
                <v-list-item-content>
                    <v-list-item-title>조회결과가 없습니다.</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
            <template v-for="(item, index) in list.items">
                <v-list-item
                    :key="item.code"
                    @click="doOpenDetail(item)"
                >
                    <v-list-item-content>
                        <v-list-item-title>{{item.name}}</v-list-item-title>
                        <v-list-item-subtitle v-html="item.addr"></v-list-item-subtitle>
                    </v-list-item-content>
                    <v-list-item-action>
                        <v-list-item-action-text>
                            <span v-if="getStatCount(item.remain_stat)">{{`${getStatCount(item.remain_stat)}개 이상`}}</span>
                        </v-list-item-action-text>
                        <v-icon
                            v-if="getStatColor(item.remain_stat)"
                            :color="getStatColor(item.remain_stat)"
                        >
                            star_border
                        </v-icon>
                    </v-list-item-action>
                </v-list-item>
                <v-divider
                    :key="index"
                ></v-divider>
            </template>
        </v-list>

        <v-overlay :value="overlay">
            <v-progress-circular indeterminate size="64"></v-progress-circular>
        </v-overlay>

        <Detail ref="detailView"/>
    </v-container>
</template>

doOpenDetail 메서드를 만들자. ref로 선언한 이름으로

detail함수 내의 doOpen 메서드를 호출하면서 item객체도 넘겨주자.

// 판매처 상세정보
doOpenDetail(item) {
	this.$refs.detailView.doOpen(item);
}

 

그럼이제 리스트를 클릭하면 sheet가 올라오면서 넘겨준 item이 뿌려질것이다.

흠...

이제 lat, lng 값으로 지도에 뿌려버리자..

 

http://apis.map.kakao.com/web/guide/

kakao map api guide대로 api키를 발급받는다.

 

1. 카카오 개발자사이트 (https://developers.kakao.com) 접속
2. 개발자 등록 및 앱 생성
3. 웹 플랫폼 추가: 앱 선택 – [설정] – [일반] – [플랫폼 추가] – 웹 선택 후 추가
4. 사이트 도메인 등록: [웹] 플랫폼을 선택하고, [사이트 도메인] 을 등록합니다. (예: http://localhost:8080)
5. 페이지 상단의 [JavaScript 키]를 지도 API의 appkey로 사용합니다.
6. 앱을 실행합니다.

 

/frontend/public/index.html 을 수정한다.

kakao sdk 파일을 script문으로 추가해준다. 뒤에 발급받은 key를 넣어주면된다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>mask finder</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
    <script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=[발급받은키]"></script>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

이제 Detail.vue에 지도를 뿌릴 map 영역을 지정해주고,

doOpen메서드 호출시에 lat, lng값으로 map을 초기화해주면 된다.

vuetify의 v-bottom-sheet 속에 있는 id=map인 div를 추가했는데,

처음에 vue쪽에서 getElementById로 했을 때 가져오지못해서 eager이란 속성을 true로 넘겨주었다.

eager : 마운트 된 컴포넌트 컨텐츠를 강제로 렌더링.
<template>
    <v-bottom-sheet v-model="sheet"
                    :eager="true">
        <v-sheet>
            <v-card class="mx-auto">
                <v-list-item>
                    <v-list-item-avatar :color="$parent.getStatColor(item.remain_stat) || 'grey'"></v-list-item-avatar>
                    <v-list-item-content>
                        <v-list-item-title class="headline">{{item.name}}</v-list-item-title>
                        <v-list-item-subtitle>입고시간: {{item.stock_at}}</v-list-item-subtitle>
                    </v-list-item-content>
                    <v-list-item-action>
                        <v-btn icon
                               :large="true"
                               @click="doClose">
                            <v-icon color="red">clear</v-icon>
                        </v-btn>
                    </v-list-item-action>
                </v-list-item>

                <v-card-text class="pb-0">
                    <h3 style="color: #fff;">{{`재고 - ${$parent.getStatCount(item.remain_stat)}개 이상`}}</h3>
                </v-card-text>
                <v-card-text>{{item.addr}}</v-card-text>

                <v-card-text>
                    <div id="map" ref="mapContainer" style="width:100%;height:400px;"></div>
                </v-card-text>
            </v-card>
        </v-sheet>
    </v-bottom-sheet>
</template>

<script>
    export default {
        name: "Detail",
        data() {
            return {
                sheet: false,

                map: null,
                item: {
                    name: null,
                    addr: null,
                    remain_stat: null,
                    lat: null,
                    lng: null
                }
            };
        },
        methods: {
            doOpen(item) {
                this.sheet = true;
                this.item = item;

                this.$nextTick(async () => {
                    const container = this.$refs.mapContainer;
                    const getLatLng = new kakao.maps.LatLng(item.lat, item.lng);
                    const mapOptions = {
                        center: getLatLng,
                        level: 3
                    };
                    this.map = new kakao.maps.Map(container, mapOptions);

                    const {default: imageSrc} = await import('@/assets/mask.png'),
                        imageSize = new kakao.maps.Size(64, 69),
                        imageOption = {offset: new kakao.maps.Point(27, 69)};

                    // 마커의 이미지정보를 가지고 있는 마커이미지를 생성
                    const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption);

                    // 마커생성
                    const marker = new kakao.maps.Marker({
                        position: getLatLng,
                        image: markerImage
                    });

                    // 마커가 지도 위에 표시되도록 설정
                    marker.setMap(this.map);
                });
            },
            doClose() {
                this.sheet = false;

                // remove map
                this.map = null;
                const container = this.$refs.mapContainer;
                while (container.lastElementChild) {
                    container.removeChild(container.lastElementChild);
                }
            }
        }
    };
</script>

<style scoped>

</style>

doOpen메서드를 보면 item을 바인딩 해준후에 바인딩을 기다리기위해 nextTick메서드 안에서 지도를 초기화했다.

getElementById대신에 ref속성으로 접근을 했고, lat, lag값으로 지도 위치를 생성해서 변수로 선언했다.

생성된 변수는 지도 센터위치와, 마커위치로 사용했다.

marker변수에 image 속성을 넘기지않으면 기본 마커이미지로 렌더링이 되는데,

마스크 png이미지를 아무거나 받아서 image를 적용시켜 보았다.

 

doClose메서드에서는 map을 초기화하는 구문을 추가했다 ㅋ

 

*주의 : eslint가 new kakao 를 정의되지않은 변수로 생각해서 package.json에서 global속성을 추가해줘야한다..

// package.json
{
	...
    "eslintConfig": {
      ...
      "globals": {
        "kakao": false
      },
    },
    ...
}

여기까지하면 이제 map도 나오고 mask 위치도 그려질 것이다. 확인해보자..

지도와 마스크..

조금 더 완성도가 있어졌다..

이제 https모드로 해서 현재위치가져오면 좀 더 좋을 것 같다.

  • 지도에 현재위치로부터 선을 그어준다거나..
  • 해당 판매처와의 남은 거리를 보여준다거나..

아니면 공공마스크 api에 반경 5km? 내의 판매처를 가져오는 api가 있다고하니,

다른 메뉴를 만들어서 반경내 판매처들을 지도에 뿌려줘도 좋을듯하다.

반응형