이전에는 공공데이터 api를 이용해서 간단하게 ui구현을 해보았다.
/storesByAddr api를 이용해서 address값을 조회조건으로 추가해보려고한다.
일단 행정구역은 api를 찾기 귀찮(?)아서 통계분류포털 에서 행정구역분류를 엑셀로 받았다.
UI쪽 조회조건 구현은 3개의 셀렉트박스를 구현할 예정이다.
ex) 첫번째 셀렉트박스엔 서울특별시, 대전광역시 등 `시` 목록이 나열.
서울특별시가 선택되어있으면 두번째 셀렉트박스엔 강남구, 은평구 등 시에 해당하는 `구` 목록이 나열.
은평구가 선택되어있으면 세번째 셀렉트박스엔 갈현동, 구산동 등 구에 해당하는 `동` 목록이 나열.
위 엑셀파일에는 분류코드와 시, 구, 동 정보가 있어서 db에 넣어서 사용하면 될듯하다.
일단 backend 와 frontend로 폴더구조를 나누었다. ㅋ
backend는 nodejs express-generator로 제공해주는 generator를 사용하여 기본구성을 했다.
단순히 시, 구, 동 정보를 제공하는 api만 만들면 되므로 간단히 해볼 예정이다.
일단 엑셀파일을 db에 넣기위해 테이블을 만들고 가공하자.
사용하기 간단한 mysql을 사용하고 workbench로 작업해보자.
위 정보를 저장해둘 테이블을 만든다. 간단히 엑셀 구조대로 코드3개와 이름3개로 구분했다.
테이블을 만들었으면 엑셀정보를 테이블에 넣기위해 가공해보자.
앞부분만 잘라서 새 sheet를 만들고 붙여넣어본다.
맨 위엔 위에서 정의한 컬럼명을 써주고 아래로 엑셀원본에서 그대로 복사해온내용 붙여넣어주자.
저장은 csv로하면되고 UTF-8로 맞춰주자.
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);
그럼 결과가 잘 나오는지 브라우저에서 확인해보겠다..
대충 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메서드를 이용해서 재고순으로 정렬해주었다.
이제 리스트를 클릭하면 상세 페이지로가서 지도에 표시해주는 정도를 구현해주면 완성도가 높아질듯하다..
'프로그래밍 > Vue.js' 카테고리의 다른 글
vuejs 가상스크롤로 로딩지연 개선 (1) | 2023.02.27 |
---|---|
vuejs 공공데이터 공적 마스크 판매 정보 api 사용해보기 (3) (0) | 2020.03.13 |
vuejs 공공데이터 공적 마스크 판매 정보 api 사용해보기 (1) (2) | 2020.03.11 |
Vue.js testing jest 사용해보기 (1) | 2019.02.14 |
Vue.js 테스트 Vuex테스트 (0) | 2019.01.06 |