세모튜브
小程序 앱만들기 - 2 : 관광정보 서비스 앱만들기(공공데이터이용) 본문
유튜브강좌 : www.youtu.be/D2y870aeOeQ
github : https://github.com/semotube/korTourInfo
tabBar 만들기
- 홈 : 첫페이지 - 인기 행사, 최근 행사, 최신 숙박업체를 표시.
- 검색 : 서비스분류, 대분류, 중분류, 소분류등의 조건을 받아 결과를 표시 (매뉴얼 참조)
- 나 : 개인정보, 분류코드 처리
- 페이지 생성
"pages":[
"pages/index/index",
"pages/search/search",
"pages/user/user",
"pages/searchStay/searchStay",
"pages/logs/logs"
],
- tabBar 생성
"tabBar": {
"backgroundColor": "#fafafa",
"borderStyle": "white",
"selectedColor": "#AB956D",
"color": "#666",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "resources/images/tabBar/home.png",
"selectedIconPath": "resources/images/tabBar/home@sel.png",
"text": "홈"
},
{
"pagePath": "pages/search/search",
"iconPath": "resources/images/tabBar/search.png",
"selectedIconPath": "resources/images/tabBar/search@sel.png",
"text": "검색"
},
{
"pagePath": "pages/user/user",
"iconPath": "resources/images/tabBar/user.png",
"selectedIconPath": "resources/images/tabBar/user@sel.png",
"text": "나"
}
]
},
- 사용되는 아이콘은 www.flaticon.com에 가입하면 무료 아이콘을 다운받을 수있으므로 적당하게 준비하면 된다.
필요한 아이콘 : 각각 선택되지않았을때와 선택 되었을때 2개의 아이콘이 필요하다.
아이콘을 다운받은 후 각각 아래의 이름으로 "resources/images/tabBar" 폴더에 저장한다.
- 홈아이콘 : home.png, home@sel.png
- 검색아이콘 : search.png, search@sel.png
- 나아이콘 : user.png, user@sel.png
각 페이지 상단 네이게이션바 처리
<!--index.wxml-->
<cu-custom bgColor="bg-gradual-pink">
<view slot="content">홈</view>
</cu-custom>
<!--search.wxml-->
<cu-custom bgColor="bg-gradual-pink">
<view slot="content">검색</view>
</cu-custom>
<!--pages/user/user.wxml-->
<cu-custom bgColor="bg-gradual-pink">
<view slot="content">나</view>
</cu-custom>
서버와 통신 Network 만들기
- OpenAPI 매뉴얼 다운로드 주소 : https://api.visitkorea.or.kr/openAPI/manual/manual.do
- api.js 만들기
utils/api.js를 생성하고 여기에 서버와의 통신관련 소스를 처리하자.
- 공통 url만들기
먼저 매뉴얼의 "가)오퍼레이션 내용"의 형식을 살펴보면 아래의 구조로 되어있다.
http://api.visitkorea.or.kr/openapi/service/rest/KorService/오퍼레이션명 <= 아래 오퍼레이션 종류 참조
공통부분 주소를 아래와 같이 url에 기억시키고
키값도 key에 기억시키자.
// api.js
const util = require("./util.js");
const url = "http://api.visitkorea.or.kr/openapi/service/rest/KorService/";
const key = "Yf5NXdsSZLmRUKZwRF01ZF83ZEVX7GG%2B54cCW3KIHC6Vb7i8OXj2vJTZAqULvt5hH5fgUkARqzcd4YyIAVG54Q%3D%3D";
- utils/util.js에 showLoading(), hideLoading(), showModal() 컴포넌트 처리하기
const showLoading = msg => {
wx.showLoading({
title: msg,
mask: true
})
}
const hideLoading = () => {
wx.hideLoading();
}
const showModal = (title, content, showCancel = true, mask = true, cancelText = "취소", confirmText ="확인") => {
wx.showModal({
title: title,
content: content,
showCancel: showCancel,
mask: mask,
cancelText: cancelText,
confirmText: confirmText,
})
}
module.exports = {
formatTime: formatTime,
showLoading,
hideLoading,
showModal,
}
- 공통 request만들기
에러코드는 아래 에러코드표 참조( 매뉴얼 참조 )
Promise :
- 자바스크립트 대표적 비동기식 http의 요청/응답 처리.
- 성공시 resolve()를 호출해서 처리
- 실패시 reject()를 호출해서 처리
function request(url, data = {}, method = "GET") {
util.showLoading('조회중...');
data.MobileOS = "ETC";
data.MobileApp = "testApp";
data._type = "json";
return new Promise(function (resolve, reject) {
wx.request({
url: url,
data: data,
method: method,
header: {
'Content-Type': 'application/json'
},
success: function (res) {
if (res.statusCode == 200) {
let resultMsg = "정상";
let resultCode = res.data.response.header.resultCode;
switch (resultCode) {
case 1: resultMsg = "어플리케이션 에러"; break;
case 2: resultMsg = "데이터베이스 에러"; break;
case 3: resultMsg = "데이터없음 에러"; break;
case 4: resultMsg = "HTTP 에러"; break;
case 5: resultMsg = "서비스 연결실패 에러"; break;
case 10: resultMsg = "잘못된 요청 파라메터 에러"; break;
case 11: resultMsg = "필수요청 파라메터가 없음"; break;
case 12: resultMsg = "해당 오픈API서비스가 없거나 폐기됨"; break;
case 20: resultMsg = "서비스 접근거부"; break;
case 21: resultMsg = "일시적으로 사용할 수 없는 서비스 키"; break;
case 22: resultMsg = "서비스 요청제한횟수 초과에러"; break;
case 30: resultMsg = "등록되지 않은 서비스키"; break;
case 31: resultMsg = "기한만료된 서비스키"; break;
case 32: resultMsg = "등록되지 않은 IP"; break;
case 33: resultMsg = "서명되지 않은 호출"; break;
case 99: resultMsg = "기타에러"; break;
}
if (resultCode != 0) {
util.hideLoading();
util.showModal("ERROR", resultMsg, false);
reject(res);
} else {
util.hideLoading();
resolve(res.data);
}
} else {
util.hideLoading();
reject(res.errMsg);
}
},
fail: function (err) {
util.hideLoading();
reject(err)
}
})
});
}
에러코드 |
에러메세지 |
설명 |
00 |
NORMAL_CODE |
정상 |
01 |
APPLICATION_ERROR |
어플리케이션 에러 |
02 |
DB_ERROR |
데이터베이스 에러 |
03 |
NODATA_ERROR |
데이터없음 에러 |
04 |
HTTP_ERROR |
HTTP 에러 |
05 |
SERVICETIMEOUT_ERROR |
서비스 연결실패 에러 |
10 |
INVALID_REQUEST_PARAMETER_ERROR |
잘못된 요청 파라메터 에러 |
11 |
NO_MANDATORY_REQUEST_PARAMETERS_ERROR |
필수요청 파라메터가 없음 |
12 |
NO_OPENAPI_SERVICE_ERROR |
해당 오픈API서비스가 없거나 폐기됨 |
20 |
SERVICE_ACCESS_DENIED_ERROR |
서비스 접근거부 |
21 |
TEMPORARILY_DISABLE_THE_SERVICEKEY_ERROR |
일시적으로 사용할 수 없는 서비스 키 |
22 |
LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR |
서비스 요청제한횟수 초과에러 |
30 |
SERVICE_KEY_IS_NOT_REGISTERED_ERROR |
등록되지 않은 서비스키 |
31 |
DEADLINE_HAS_EXPIRED_ERROR |
기한만료된 서비스키 |
32 |
UNREGISTERED_IP_ERROR |
등록되지 않은 IP |
33 |
UNSIGNED_CALL_ERROR |
서명되지 않은 호출 |
99 |
UNKNOWN_ERROR |
기타에러 |
- 오퍼레이션 호출용 주소 만들기
오퍼레이션 종류는 오퍼레이션표 참조( 매뉴얼 참조 )
기본적인 구조는 "공통url + 오퍼레이션 + API 키값" 이다.
module.exports = {
request,
AreaCode: url + "areaCode" + "?serviceKey=" + key,//지역코드조회
CategoryCode: url + "categoryCode" + "?serviceKey=" + key,//서비스 분류코드 조회
AreaBasedList: url + "areaBasedList" + "?serviceKey=" + key,
LocationBasedList: url + "locationBasedList" + "?serviceKey=" + key,
SearchKeyword: url + "searchKeyword" + "?serviceKey=" + key,
SearchFestival: url + "searchFestival" + "?serviceKey=" + key,//행사정보 조회
SearchStay: url + "searchStay" + "?serviceKey=" + key,//숙박정보 조회
DetailCommon: url + "detailCommon" + "?serviceKey=" + key,
DetailIntro: url + "detailIntro" + "?serviceKey=" + key,
DetailInfo: url + "detailInfo" + "?serviceKey=" + key,
DetailImage: url + "detailImage" + "?serviceKey=" + key,
}
번호 |
서비스명(국문) |
오퍼레이션명(영문) |
오퍼레이션명(국문) |
1 |
국문 관광정보 서비스 |
areaCode |
지역코드조회 |
2 |
categoryCode |
서비스 분류코드 조회 |
|
3 |
areaBasedList |
지역기반 관광정보 조회 |
|
4 |
locationBasedList |
위치기반 관광정보 조회 |
|
5 |
searchKeyword |
키워드 검색 조회 |
|
6 |
searchFestival |
행사정보 조회 |
|
7 |
searchStay |
숙박정보 조회 |
|
8 |
detailCommon |
공통정보 조회 (상세정보1) |
|
9 |
detailIntro |
소개정보 조회 (상세정보2) |
|
10 |
s |
반복정보 조회 (상세정보3) |
|
11 |
detailImage |
이미지정보 조회 (상세정보4) |
홈 소스 만들기 : index.js
공용 함수와 데이타 정의
//index.js
const api = require("../../utils/api.js");
const util = require("../../utils/util.js");
const app = getApp();
Page({
data: {
festivalList_Hot: [],//이미지 있는 조회순(인기순)
festivalList_New: [],//수정일순(최신순)
stayList_New: [],//수정일순(최신순)
},
데이터를 가져오기 위해 API Network 함수 호출하기
onLoad: function () {
let toDay = util.getToday();
this.searchFestival("P", toDay, 10);//이미지 있는 조회순(인기순)
this.searchFestival("C", toDay, 6);//수정일순(최신순)
this.searchStay("C", toDay, 6);//수정일순(최신순)
},
오늘날자를 얻기위한 utils/util.js => getToday()작성
const getToday = () => {
let date = new Date();//new Date('December 25, 1995 23:15:30');
return [date.getFullYear(), date.getMonth() + 1, date.getDate()].map(formatNumber).join('');//join('-');
}
module.exports = {
formatTime: formatTime,
getToday: getToday,
showToast,
hideToast,
showLoading,
hideLoading,
showModal,
}
API Network함수 작성
14) 행사정보 조회 오퍼레이션 명세 => 매뉴얼 참조
searchFestival( 파라메터 ) { ... }
- eventStartDate : 행사 시작일 (형식 : YYYYMMDD)
- arrange 파라메터 :
- (A=제목순, B=조회순, C=수정일순, D=생성일순)
- 대표이미지가 반드시 있는 정렬
- (O=제목순, P=조회순, Q=수정일순, R=생성일순)
- numOfRows : 결과 수
searchFestival(arrange = "", eventStartDate = "", numOfRows = 10) {
let that = this;
api.request(api.SearchFestival, {
arrange: arrange,
eventStartDate: eventStartDate,
numOfRows: numOfRows,
}).then(function (res) {
if (res.response.header.resultMsg === "OK") {
let item = res.response.body.items.item;
if (!Array.isArray(item)) item = [item];
// console.log("item : ", item)
that.setData({
festivalList_Hot: arrange === "P" ? item : that.data.festivalList_Hot,
festivalList_New: arrange === "C" ? item : that.data.festivalList_New,
})
util.hideLoading();
} else { console.error("resultMsg != 'OK' : ", res.response.header.resultMsg) }
})
.catch(function (res) {
util.hideLoading();
console.error("catch : ", res)
})
},
15) 숙박정보 조회 오퍼레이션 명세 => 매뉴얼 참조
searchStay( 파라메터 ){ ... }
- eventStartDate : 행사 시작일 (형식 : YYYYMMDD)
- arrange 파라메터 :
- (A=제목순, B=조회순, C=수정일순, D=생성일순)
- 대표이미지가 반드시 있는 정렬
- (O=제목순, P=조회순, Q=수정일순, R=생성일순)
- numOfRows : 결과 수
searchStay(arrange = "", eventStartDate = "", numOfRows = 10) {
let that = this;
api.request(api.SearchStay, {
arrange: arrange,
eventStartDate: eventStartDate,
numOfRows: numOfRows,
}).then(function (res) {
if (res.response.header.resultMsg === "OK") {
let item = res.response.body.items.item;
if (!Array.isArray(item)) item = [item];
// console.log("item : ", item)
that.setData({
stayList_Hot: arrange === "P" ? item : that.data.stayList_Hot,
stayList_New: arrange === "C" ? item : that.data.stayList_New,
})
util.hideLoading();
} else { console.error("resultMsg != 'OK' : ", res.response.header.resultMsg) }
})
.catch(function (res) {
util.hideLoading();
console.error("catch : ", res)
})
},
홈 레이아웃 만들기 : index.wxml
<!--index.wxml-->
<cu-custom bgColor="bg-gradual-pink">
<view slot="content">홈</view>
</cu-custom>
<!-- Hot Festival swiper -->
<swiper class="screen-swiper round-dot" indicator-dots="true" circular="true" autoplay="true" interval="5000" duration="500">
<swiper-item wx:for="{{festivalList_Hot}}" wx:key="*this">
<image src="{{item.firstimage}}" mode="scaleToFill"></image>
</swiper-item>
</swiper>
<!-- New Festival -->
<view class="cu-bar bg-white margin-top solids-bottom">
<view class="action sub-title">
<text class="text-xl text-bold text-green">행사정보</text>
<text class="text-ABC text-green">Festival</text>
</view>
<view class='action'>
<text class="cuIcon-more"></text>
</view>
</view>
<view class="cu-list grid col-2">
<view class="cu-item" wx:for="{{festivalList_New}}" wx:key="*this" bindtap="goDetail" data-idx="{{index}}">
<view class="bg-pink padding radius text-center shadow-blur">
<image src="{{item.firstimage2}}" mode="aspectFit" class="radius" style="height:300rpx"></image>
<view class="text-cut margin-top-sm text-Abc">{{item.title}}</view>
</view>
</view>
</view>
<!-- New Stay -->
<view class="cu-bar bg-white margin-top solids-bottom">
<view class="action sub-title">
<text class="text-xl text-bold text-green">숙박정보</text>
<text class="text-ABC text-green">STAY</text>
</view>
<view class='action'>
<text class="cuIcon-more"></text>
</view>
</view>
<view class="cu-list grid col-2">
<view class="cu-item" wx:for="{{stayList_New}}" wx:key="*this" bindtap="goDetail" data-idx="{{index}}">
<view class="bg-pink padding radius text-center shadow-blur">
<image src="{{item.firstimage2}}" mode="aspectFit" class="radius" style="height:300rpx"></image>
<view class="text-cut margin-top-sm text-Abc">{{item.title}}</view>
</view>
</view>
</view>
/**index.wxss**/
.cu-list.grid>.cu-item {
position: relative;
display: flex;
padding: 20rpx;
transition-duration: 0s;
flex-direction: column
}
기타 소스 확인
실제 핸드폰 테스트시 wx.getMenuButtonBoundingClientRect()값이 전부 0이 자주 나옴.
0이 나오면 상단 네비게이션바 부분에 에러가 발생한다.
우선 아래 소스 추가하여 강제로 처리하자.
정상처리
//app.js
App({
onLaunch: function () {
// wx.getSystemInfo({
// success: e => {
// // console.log(e);
// this.globalData.StatusBar = e.statusBarHeight;
// let custom = wx.getMenuButtonBoundingClientRect();
// console.log('custom : ', custom);
// this.globalData.Custom = custom;
// this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight;
// }
// });
// 2.11.0버젼: getMenuButtonBoundingClientRect()값이 전부 0이 자주 나옴(핸드폰 테스트시)
// 우선 아래 소스를 이용해서 처리.
wx.getSystemInfo({
success: e => {
if (!wx.canIUse('getMenuButtonBoundingClientRect')) {
wx.showModal({ title: 'Error', content: "can't use getMenuButtonBoundingClientRect()" })
}
let IS_ERROR = false;
let custom = null;
try {
custom = wx.getMenuButtonBoundingClientRect();
if (custom && custom.height) {
if (custom.bottom == 0 || custom.top == 0) { IS_ERROR = true; }
else { wx.setStorageSync("customInfo", custom) }
} else { IS_ERROR = true; }
} catch (e) { IS_ERROR = true; }
let defaultCustom = { width: 87, height: 32, left: 317, top: 50, right: 404, bottom: 82 }
if (IS_ERROR) { custom = wx.getStorageSync("customInfo") || defaultCustom }
this.globalData.StatusBar = e.statusBarHeight;
this.globalData.Custom = custom;
this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight;
this.globalData.BoxHeight = e.windowHeight - this.globalData.CustomBar;
this.globalData.ISIPX = false;
let x = e.model.toLowerCase().replace(/ /g, '');
if (x.indexOf('iphonex') > -1 || x.indexOf('iphone1') > -1) {
this.globalData.BoxHeight = this.globalData.BoxHeight - 34; //iphone x
this.globalData.ISIPX = true;
}
},
});
},
globalData: {
}
})
/**app.wxss**/
@import "colorui/main.wxss";
@import "colorui/icon.wxss";
'미니프로그램-小程序 > 위챗-관광정보 앱' 카테고리의 다른 글
小程序 앱만들기 - 6 : 코드관리 페이지 만들기 - <picker>컴포넌트 사용 (0) | 2020.05.18 |
---|---|
小程序 앱만들기 - 5 : 로그인/아웃 및 <navigator> 컴포넌트 사용 (0) | 2020.05.14 |
小程序 앱만들기 - 4 : "나" 페이지 만들기 (0) | 2020.05.13 |
小程序 앱만들기 - 3 : 외부와 통신이 않될때 처리하는 방법 (0) | 2020.05.13 |
小程序 앱만들기 - 1 : 관광정보 서비스 앱만들기(공공데이터이용) (0) | 2020.04.29 |