프로그래밍/Vue.js

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

p-a-r-k 2020. 3. 12. 16:08
반응형

이전에는 공공데이터 api를 이용해서 간단하게 ui구현을 해보았다.

/storesByAddr api를 이용해서 address값을 조회조건으로 추가해보려고한다.

일단 행정구역은 api를 찾기 귀찮(?)아서 통계분류포털 에서 행정구역분류를 엑셀로 받았다.

다운받은 2020기준 엑셀파일..ㅋ

UI쪽 조회조건 구현은 3개의 셀렉트박스를 구현할 예정이다.

ex) 첫번째 셀렉트박스엔 서울특별시, 대전광역시 등 `시` 목록이 나열.

서울특별시가 선택되어있으면 두번째 셀렉트박스엔 강남구, 은평구 등 시에 해당하는 `구` 목록이 나열.

은평구가 선택되어있으면 세번째 셀렉트박스엔 갈현동, 구산동 등 구에 해당하는 `동` 목록이 나열.

위 엑셀파일에는 분류코드와 시, 구, 동 정보가 있어서 db에 넣어서 사용하면 될듯하다.

일단 backend 와 frontend로 폴더구조를 나누었다. ㅋ

backend는 nodejs express-generator로 제공해주는 generator를 사용하여 기본구성을 했다.

단순히 시, 구, 동 정보를 제공하는 api만 만들면 되므로 간단히 해볼 예정이다.

 

일단 엑셀파일을 db에 넣기위해 테이블을 만들고 가공하자.

사용하기 간단한 mysql을 사용하고 workbench로 작업해보자.

위 정보를 저장해둘 테이블을 만든다. 간단히 엑셀 구조대로 코드3개와 이름3개로 구분했다.

테이블 만들기 ㅋ

테이블을 만들었으면 엑셀정보를 테이블에 넣기위해 가공해보자. 

앞부분만 잘라서 새 sheet를 만들고 붙여넣어본다.

맨 위엔 위에서 정의한 컬럼명을 써주고 아래로 엑셀원본에서 그대로 복사해온내용 붙여넣어주자.

저장은 csv로하면되고 UTF-8로 맞춰주자.

import할 가공 된 엑셀

workbench에서는 db를 우클릭해서 table data import wizard를 눌러주자.

가공한 csv를 불러와서 next 계속 눌러주면 아래와 같은 화면을 볼 수 있다.

각 컬럼에 내용이 매칭되는걸 볼 수 있다 ㅋ

다만, 1, 2번째 줄처럼 구, 동이 없는 데이터가 들어가는데.

해당하는 정보는 delete처리 해주면 된다.

delete from [테이블명] where TYPE_CODE2 = '' OR TYPE_CODE3 = '';

delete를 잘 했으면 select 했을 때 아래와같이 나오면 된다.

테이블에 잘 들어갔다..

이제 express에서 get 요청 route를 만들어주면 된다.. 

단순하게 그냥 3가지 경우에 대하여 query만 정의해서 조회결과만 가져보여주는 api를 만들었다.

  • /area/1 : `시` 정보를 가져온다.
  • /area/2 : `시`에대한 `구`정보를 가져온다.
  • /area/3 : `구`에대한 `동`정보를 가져온다.
// routes/area.js
const express = require('express');
const router = express.Router();

const mysql = require('mysql');
// Connection 객체 생성
const connection = mysql.createConnection({
  host: 'localhost',
  port: 3306,
  user: 'root',
  password: 'password',
  database: 'park'
});
// Connect
connection.connect(function (err) {
  if (err) {
    console.error('mysql connection error');
    console.error(err);
    throw err;
  }
});

router.get('/:type', function (req, res) {
  let setQuery = '';
  switch (req.params.type) {
    case '1':
      setQuery = 'select TYPE_CODE1, AREA_NAME1 from area_info group by TYPE_CODE1';
      break;
    case '2':
      setQuery = 'select TYPE_CODE2, AREA_NAME2 from area_info where TYPE_CODE1 = \''+ req.query.code +'\' group by TYPE_CODE2';
      break;
    case '3':
      setQuery = 'select TYPE_CODE3, AREA_NAME3 from area_info where TYPE_CODE3 = \''+ req.query.code +'\' group by TYPE_CODE3';
      break;
    default:
      res.status(404).send('잘못된 접근입니다.');
      break;
  }
  const query = connection.query(
      setQuery,
      function (err, result) {
        if (err) {
          console.error(err);
          res.status(500).send('정보를 가져올 수 없습니다. '+ req.params.type);
        }
        res.status(200).send(result);
      }
  );
});

module.exports = router;

app.js에서는 area.js를 use해주면 끝이다.

app.use('/area', areaRouter);

그럼 결과가 잘 나오는지 브라우저에서 확인해보겠다..

/area/1
/area/2?code=11

대충 api가 만들어진거같으니 UI에 조회조건을 이제 추가해보자..

기존 mask객체 메서드 path앞에 /openapi/를 추가하였다.

proxy분리를 위함인데 /openapi/인경우 공공api 주소로, /api/인경우 express 서버로 proxy하기 위함이다.

// frontend/src/api/index.js
import api from '@/api/common';

function jsonToQueryString(json) {
    if (!json) return '';
    return '?' +
        Object.keys(json).map(function (key) {
            if (json[key] instanceof Array) {
                let query = [];
                for (let i = 0;i < json[key].length;i++) {
                    query.push(encodeURIComponent(key) + '=' +
                             encodeURIComponent(json[key][i]));
                }
                return query.join('&');
            } else {
                return encodeURIComponent(key) + '=' +
                    encodeURIComponent(json[key]);
            }
        }).join('&');
}

export const mask = {
    fetchByAddr(data) {
        return api.request({
            method: 'get',
            url: `/openapi/storesByAddr/json${jsonToQueryString(data)}`
        });
    }
};

export const area = {
    fetchCity() { // 시 정보
        return api.request({
            method: 'get',
            url: `/area/1`
        });
    },
    fetchDistrict(code) { // 구 정보
        return api.request({
            method: 'get',
            url: `/area/2?code=${code}`
        });
    },
    fetchNeigh(code) { // 동 정보
        return api.request({
            method: 'get',
            url: `/area/3?code=${code}`
        });
    }
};

export default mask;
// frontend/src/api/common.js
import axios from 'axios';

export const SETTINGS = {
    DOMAIN: '/api',
    UNAUTHORIZED: 401
};

// 기본 타임아웃
axios.defaults.timeout = 1000000;
axios.defaults.withCredentials = true;

export default {
    request (settings) {
        const GET_URL = settings.url;
        // 공공api가 아닌경우 url앞에 /api 추가
        if (!GET_URL.match('/openapi/')) {
            settings.url = SETTINGS.DOMAIN + GET_URL;
        }

        return axios(settings)
            .then(result => result)
            .catch(err => {
                throw err;
            });
    }
};

마지막으로 vue.config.js의 proxy설정도 수정해준다.

module.exports = {
    "transpileDependencies": [
        "vuetify"
    ],
    outputDir: '../backend/public',
    devServer: {
        disableHostCheck: true,
        proxy: {
            '/openapi/': {
                target: "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1",
                changeOrigin: true,
                pathRewrite: {
                    '^/openapi': ''
                }
            },
            '/api/': {
                target: "http://localhost:3000",
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
};

설정은 완료했으니 Main.vue 파일에 api연동해서 조회조건을 구현해준다.

새로고침해도 최신 state 유지를위해 store쪽 state를 사용했다.

<template>
    <v-container fluid>
        <v-card
            class="mx-auto"
        >
            <v-card-text>
                <v-container fluid>
                    <v-row align="center">
                        <v-col class="d-flex" cols="12" sm="4">
                            <v-select
                                :value="search.city"
                                @input="updateCity"
                                :items="getCity"
                                item-text="AREA_NAME1"
                                item-value="TYPE_CODE1"
                                label="시·도"
                                hide-details
                                outlined
                            ></v-select>
                        </v-col>
                        <v-col class="d-flex" cols="12" sm="4">
                            <v-select
                                :value="search.district"
                                @input="updateDistrict"
                                @change="doFetchMask"
                                :items="getDistrict"
                                item-text="AREA_NAME2"
                                item-value="TYPE_CODE2"
                                label="시·군·구"
                                hide-details
                                outlined
                            ></v-select>
                        </v-col>
                        <v-col class="d-flex" cols="12" sm="4">
                            <v-select
                                :value="search.neigh"
                                @input="updateNeigh"
                                @change="doFetchMask"
                                :items="getNeigh"
                                item-text="AREA_NAME3"
                                item-value="TYPE_CODE3"
                                label="읍·면·동"
                                hide-details
                                outlined
                            ></v-select>
                        </v-col>
                    </v-row>
                </v-container>
            </v-card-text>
        </v-card>
        <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="() => {}"
                >
                    <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>
    </v-container>
</template>

<script>
    import {mapState} from 'vuex';
    import {
        mask,
        area
    } from '@/api';

    export default {
        name: "Main",
        data() {
            return {
                overlay: false,
                area: {
                    city: [],
                    district: [],
                    neigh: []
                },
                list: {
                    items: []
                }
            };
        },
        computed: {
            ...mapState(['search']),
            getCity() {
                const getList = this.area.city;
                getList.unshift({TYPE_CODE1: null, AREA_NAME1: '선택하세요'});
                return getList;
            },
            getDistrict() {
                const getList = this.area.district;
                getList.unshift({TYPE_CODE2: null, AREA_NAME2: '선택하세요'});
                return getList;
            },
            getNeigh() {
                const getList = this.area.neigh;
                getList.unshift({TYPE_CODE3: null, AREA_NAME3: '전체'});
                return getList;
            }
        },
        watch: {
            'search.city'(val) {
                this.$store.commit('SET_DISTRICT', null);
                this.$store.commit('SET_NEIGH', null);
                this.area.district = [];
                this.area.neigh = [];
                this.list.items = [];

                if (val) this.doFetchDistrict();
            },
            'search.district'(val) {
                this.$store.commit('SET_NEIGH', null);
                this.area.neigh = [];

                if (val) this.doFetchNeigh();
            }
        },
        methods: {
            getStatCount(stat) {
                switch (stat) {
                    case "plenty": return 100;
                    case "some": return 30;
                    case "few": return 2;
                    case "empty": return 0;
                    default: return null;
                }
            },
            getStatColor(stat) {
                switch (stat) {
                    case "plenty": return 'green';
                    case "some": return 'yellow';
                    case "few": return 'red';
                    case "empty": return '';
                }
            },

            // 마스크 정보 가져오기
            doFetchMask() {
                let address = '';
                const {city, district, neigh} = this.search;
                const getCity = city && this._.find(this.area.city, (e) => e.TYPE_CODE1 === city);
                const getDistrict = district && this._.find(this.area.district, (e) => e.TYPE_CODE2 === district);
                const getNeigh = neigh && this._.find(this.area.neigh, (e) => e.TYPE_CODE3 === neigh);
                if (getCity) address += getCity.AREA_NAME1;
                if (getDistrict) address += ` ${getDistrict.AREA_NAME2}`;
                if (getNeigh) address += ` ${getNeigh.AREA_NAME3}`;

                this.overlay = true;
                mask
                    .fetchByAddr({address})
                    .then(res => res.data)
                    .then(items => {
                        if (items.stores.length > 0) {
                            this.list.items = items.stores.sort((a, b) => this.getStatCount(b.remain_stat) - this.getStatCount(a.remain_stat));
                        } else {
                            this.list.items = [];
                        }
                    }).catch(console.log)
                    .finally(() => {
                        this.overlay = false;
                    });
            },

            // 시,도 정보 가져오기
            doFetchCity() {
                return area
                    .fetchCity()
                    .then(res => res.data)
                    .then(list => {
                        if (list.length) {
                            this.area.city = list;
                        } else {
                            this.area.city = [];
                        }
                    }).catch(console.log);
            },
            // 시, 군 정보 가져오기
            doFetchDistrict() {
                return area
                    .fetchDistrict(this.search.city)
                    .then(res => res.data)
                    .then(list => {
                        if (list.length) {
                            this.area.district = list;
                        } else {
                            this.area.district = [];
                        }
                    }).catch(console.log);
            },
            // 읍면동 정보 가져오기
            doFetchNeigh() {
                return area
                    .fetchNeigh(this.search.district)
                    .then(res => res.data)
                    .then(list => {
                        if (list.length) {
                            this.area.neigh = list;
                        } else {
                            this.area.neigh = [];
                        }
                    }).catch(console.log);
            },

            updateCity(e) {
                this.$store.commit('SET_CITY', e);
            },

            updateDistrict(e) {
                this.$store.commit('SET_DISTRICT', e);
            },

            updateNeigh(e) {
                this.$store.commit('SET_NEIGH', e);
            }
        },
        async created() {
            await this.doFetchCity();
            await this.doFetchDistrict();
            await this.doFetchNeigh();

            this.doFetchMask();
        }
    };
</script>

<style scoped>

</style>

각 3가지 셀렉트박스에 대한 호출과 change이벤트를 구현해주고 mask정보를 다시 fetch해주면 된다.

mask정보를 가져오고난 후 sort메서드를 이용해서 재고순으로 정렬해주었다.

구현된 ui화면 ㅋ

이제 리스트를 클릭하면 상세 페이지로가서 지도에 표시해주는 정도를 구현해주면 완성도가 높아질듯하다..

반응형