LifeSports Application(ReactNative & Nest.js) - 14. API gateway 연동, map-service(2)
#1 client, API gateway 연동
이전 포스트에서 kong을 이용한 api gateway를 구현해보았습니다. 그러면 이 API Server와 react native 클라이언트를 연동하여 하나의 포트로 여러 서비스들과의 통신을 구현하겠습니다.
react native의 package.json의 proxy를 다음과 같이 변경하겠습니다.
- package.json
{
"scripts": {
"android": "react-native run-android adb reverse tcp:8081 tcp:8000",
...
},
"proxy": "http://10.0.2.2:8000"
}
그리고 api 디렉토리의 파일을 수정하겠습니다.
- ./src/lib/api/auth.js
import client from './client';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const login = ({
email,
password
}) => client.post('http://10.0.2.2:8000/auth-service/login', {
email,
password
});
export const register = ({
email,
password,
phoneNumber,
nickname
}) => client.post('http://10.0.2.2:8000/auth-service/register', {
email,
password,
phoneNumber,
nickname
});
export const getUser = async userId => client.get(`http://10.0.2.2:8000/auth-service/${userId}`, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(await AsyncStorage.getItem('token'))
}
});
export const check = async userId => client.get(`http://10.0.2.2:8000/auth-service/${userId}/check`, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(await AsyncStorage.getItem('token'))
}
});
export const logout = () => client.post('http://10.0.2.2:8000/auth-service/logout');
auth-service의 ResponseUser에 token 필드를 추가하고 AppController에서 로그인 시 반환되는 값을 다음과 같이 수정하겠습니다.
- ./src/app.controller.ts
import { Body, Controller, Get, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
import { Builder } from "builder-pattern";
import { AuthService } from "./auth/auth.service";
import { statusConstants } from "./constants/status.constant";
import { UserDto } from "./dto/user.dto";
import { JwtAuthGuard } from "./guard/jwt-auth.guard";
import { LocalAuthGuard } from "./guard/local-auth.guard";
import { UserService } from "./user/user.service";
import { RequestLogin } from "./vo/request.login";
import { RequestRegister } from "./vo/request.register";
import { ResponseUser } from "./vo/response.user";
@Controller('auth-service')
export class AppController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
...
@UseGuards(LocalAuthGuard)
@Post('login')
public async login(@Body() requestLogin: RequestLogin): Promise<any> {
try {
const result = await this.authService.login(Builder(UserDto).email(requestLogin.email)
.build());
if(result.status === statusConstants.ERROR) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + result.message,
});
}
return await Object.assign({
status: HttpStatus.OK,
payload: Builder(ResponseUser).email(result.payload.email)
.nickname(result.payload.nickname)
.phoneNumber(result.payload.phoneNumber)
.userId(result.payload.userId)
.token(result.access_token)
.build(),
message: 'Successfully Login'
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + err
});
}
}
...
}
그러면 react native를 실행시키고, 테스트를 진행해보겠습니다.
#2 auth-service 연동 테스트
결과 화면처럼 로그인이 잘 되는 모습을 확인할 수 있습니다. 그러면 앞서 작성한 map-service 또한 이와 같이 연동을 하여 map 데이터도 받아올 수 있겠죠.
map-service, wayfinding-service 백엔드는 구현이 완료되었으니 client에서 맵 관련 리덕스 모듈을 작성하도록 하겠습니다.
#3 marker, map 리덕스
map에서 marker는 찾고자 하는 데이터가 어디에 있는지 보여주는 시인성이 높은 요소입니다. 대관 애플리케이션에서 marker는 다음의 기능을 수행합니다.
1) marker 클릭 시 하단에 장소와 관련된 인포그래픽이 생성됩니다.
2) 인포그래픽에는 장소의 정보가 적혀있으며 대관하기 버튼이 있습니다.
3) 대관하기 버튼이 클릭되면 상세 정보, 대관과 관련된 기능을 수행할 수 있습니다.
상기 기능들을 수행하기 위해서는 인포그래픽 생성이 우선입니다. 그래서 마커를 클릭하게 되면 리덕스로 만들 visible 플래그 state 값에 true, false가 들어가게 되며 true시 인포그래픽에 30% 높이, 맵의 크기는 60%의 높이를 갖게 만들도록 하겠습니다.
그리고 map에서 marker를 위한 맵 데이터는 전체 리스트를 가져와 marker에 하나씩 데이터를 부여하기 때문에 map 하나의 정보를 담을 map redux도 구현하도록 하겠습니다.
우선 marker를 만들고 그 다음 인포그래픽 생성부터 구현을 하도록 하겠습니다.
- ./src/modules/marker.js
import { createAction, handleActions } from "redux-actions";
const CHANGE_VISIBLE = 'marker/VISIBLE';
export const changeState = createAction(
CHANGE_VISIBLE,
value => value
);
const initialState = {
visible: null
};
const marker = handleActions(
{
[CHANGE_VISIBLE]: (state, { payload: value }) => ({
...state,
visible: value
}),
},
initialState,
);
export default marker;
- ./src/modules/map.js
import { createAction, handleActions } from "redux-actions";
const [CHANGE_MAP] = 'map/CHANGE_MAP';
export const changeMap = createAction(
CHANGE_MAP,
value => value
);
const initialState = {
map: null,
error: null,
};
const map = handleActions(
{
[CHANGE_MAP]: (state, { payload: map }) => ({
...state,
map,
}),
},
initialState,
);
export default map;
- ./src/modules/index.js
import { combineReducers } from "redux";
import { all } from "redux-saga/effects";
import auth, { authSaga } from './auth';
import loading from "./loading";
import user, { userSaga } from "./user";
import marker from './marker';
import map from "./map";
const rootReducer = combineReducers({
auth,
loading,
user,
marker,
map,
});
export function* rootSaga() {
yield all([
authSaga(),
userSaga(),
]);
};
export default rootReducer;
visible값을 리덕스 state값으로 만들어 marker를 클릭하게 되면 visible에 true, 닫기 버튼을 누르게 되면 false 값을 부여합니다. 그리고 map에 해당 marker에 존재하는 맵 데이터로 변경합니다.
- ./src/pages/map/components/CustomMarker.js
import React from "react"
import { useDispatch } from 'react-redux';
import { View } from "react-native";
import { Marker } from "react-native-nmap";
import markerImage from '../../../assets/img/markerImage.png';
import palette from "../../../styles/palette";
import { changeState } from "../../../modules/marker";
import { changeMap } from "../../../modules/map";
const CustomMarker = ({ data }) => {
const dispatch = useDispatch();
const coordinate = {
latitude: data.ycode,
longitude: data.xcode,
};
const onVisible = e => {
e.preventDefault();
dispatch(changeState(true));
dispatch(changeMap(data));
};
return(
<View>
<Marker coordinate={ coordinate }
image={ markerImage }
pinColor={ palette.blue[4] }
caption={{
text: data.nm,
textSize: 13,
}}
onClick={ onVisible }
/>
</View>
);
};
export default CustomMarker;
- ./src/pages/map/components/NaverMap.js
import React from 'react';
import { StyleSheet } from 'react-native';
import Loading from '../../../styles/common/Loading';
import NaverMapView from 'react-native-nmap';
import CustomMarker from './CustomMarker';
import { useSelector } from 'react-redux';
const NaverMap = () => {
const { visible } = useSelector(({ marker }) => ({ visible: marker.visible }));
const defaultLocation = {
latitude: 37.6009735,
longitude: 126.9484764
};
const dummyData = [
{"ycode":37.6144169,"type_nm":"구기체육관","gu_nm":"중랑구","parking_lot":"주차 가능(일반 18면 / 장애인 2면)","bigo":"","xcode":127.0842018,"tel":"949-5577","addr":"중랑구 숙선옹주로 66","in_out":"실내","home_page":"http://www.jungnangimc.or.kr/","edu_yn":"유","nm":"묵동다목적체육관"},
{"ycode":37.573171,"type_nm":"골프연습장","gu_nm":"중랑구","parking_lot":"용마폭포공원 주차장 이용(시간당 1,200원 / 5분당 100원)","bigo":"","xcode":127.0858392,"tel":"490-0114 ","addr":"중랑구 용마산로 217","in_out":"실내","home_page":"http://www.jjang.or.kr/jjang/","edu_yn":"유","nm":"중랑청소년수련관 골프연습장"},
{"ycode":37.580646,"type_nm":"수영장","gu_nm":"중랑구","parking_lot":"홈플러스 면목점 B동 이용, 겸재로 2길 거주자 우선 주차 구역 이용(36면) ","bigo":"","xcode":127.0773483,"tel":"435-0990","addr":"중랑구 면목동 중랑천 장안교 ","in_out":"실외","home_page":"http://jungnangimc.or.kr/","edu_yn":"무","nm":"중랑천물놀이시설"},
{"ycode":37.6058844,"type_nm":"수영장","gu_nm":"중랑구","parking_lot":"주차 가능","bigo":"","xcode":127.1088479,"tel":"492-7942","addr":"중랑구 송림길 156","in_out":"실내","home_page":"http://mangwoo.kr/","edu_yn":"유","nm":"망우청소년수련관 수영장"},
{"ycode":37.5792399,"type_nm":"수영장","gu_nm":"중랑구","parking_lot":"주차 가능(51면)","bigo":"","xcode":127.0959499,"tel":"436-9200","addr":"중랑구 사가정로72길 47","in_out":"실내","home_page":"http://jungnangspo.seoul.kr","edu_yn":"유","nm":"중랑문화체육관 수영장"},
{"ycode":37.6151721,"type_nm":"수영장","gu_nm":"중랑구","parking_lot":"주차 가능(36면)","bigo":"","xcode":127.0874763,"tel":"3423-1070","addr":"중랑구 신내로15길 189","in_out":"실내","home_page":"http://jungnangspo.seoul.kr","edu_yn":"유","nm":"중랑구민체육센터 수영장"},
{"ycode":37.573171,"type_nm":"생활체육관","gu_nm":"중랑구","parking_lot":"용마폭포공원 주차장 이용(시간당 1,200원 / 5분당 100원)","bigo":"","xcode":127.0858392,"tel":"490-0114 ","addr":"중랑구 용마산로 217","in_out":"실내","home_page":"http://www.jjang.or.kr/jjang/","edu_yn":"유","nm":"중랑청소년수련관"},
{"ycode":37.6058844,"type_nm":"생활체육관","gu_nm":"중랑구","parking_lot":"주차 가능","bigo":"","xcode":127.1088479,"tel":"492-7942","addr":"중랑구 송림길 156","in_out":"실내","home_page":"http://http://mangwoo.kr/","edu_yn":"유","nm":"망우청소년수련관"},
{"ycode":37.5878763,"type_nm":"생활체육관","gu_nm":"중랑구","parking_lot":"공영주차장 회원 2시간 무료","bigo":"","xcode":127.0808914,"tel":"495-5200","addr":"중랑구 겸재로 23길 27","in_out":"실내","home_page":"http://jungnangspo.seoul.kr","edu_yn":"유","nm":"면목2동체육관"},
];
return(
<NaverMapView style={
visible ?
styles.openInfoContainer :
styles.closeInfoContainer
}
showsMyLocationButton={ true }
center={{
...defaultLocation,
zoom: 15,
}}
scaleBar={ true }
>
{
dummyData ?
dummyData.map(
(map, i) => {
return <CustomMarker key={ i }
data={ map }
/>
}
) : <Loading />
}
</NaverMapView>
);
};
const styles = StyleSheet.create({
openInfoContainer: {
width: '100%',
height: '60%'
},
closeInfoContainer: {
width: '100%',
height: '90%',
},
});
export default NaverMap;
marker를 완성했으니 인포그래픽도 구현하고 marker 클릭 시 인포그래픽이 잘 생성되는지 테스트를 진행하도록 하겠습니다.
- ./src/pages/map/components/InfoClose.js
import React from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
import Icon from 'react-native-vector-icons/Ionicons';
import { useDispatch, useSelector } from "react-redux";
import { changeState } from "../../../modules/marker";
import palette from "../../../styles/palette";
const InfoClose = () => {
const { map } = useSelector(({ map }) => ({ map: map.map }));
const dispatch = useDispatch();
const onClose = e => {
e.preventDefault();
dispatch(changeState(false));
};
return (
<View style={ styles.container } >
<View style={ styles.type_article }>
<Text style={ styles.type_font }>
{ map.type_nm }
</Text>
</View>
<TouchableOpacity onPress={ onClose } >
<Icon name={ 'ios-close-sharp' }
size={ 25 }
color={ palette.blue[4] }
/>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
width: '100%',
height: '15%',
marginTop: 10,
paddingRight: 10,
},
type_article: {
width: '88%',
marginLeft: 15,
},
type_font: {
fontWeight: 'bold'
},
});
export default InfoClose;
닫기 버튼을 위한 컴포넌트입니다.
- ./src/pages/map/components/Info.js
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
import palette from "../../../styles/palette";
const Info = () => {
const { map } = useSelector(({ map }) => ({ map: map.map }));
return(
<View style={ styles.container } >
<View style={ styles.title } >
<Text style={ styles.place_name } >
{ map.nm }
</Text>
</View>
<View style={ styles.address } >
<Text>
{ map.addr }
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
height: '45%',
borderBottomColor: palette.gray[3],
borderBottomWidth: 1,
},
title: {
flexDirection: 'column',
justifyContent: 'flex-start',
margin: 15,
},
place_name: {
fontWeight: 'bold',
fontSize: 20,
},
address: {
justifyContent: 'flex-start',
marginLeft: 15,
},
});
export default Info;
인포그래픽에 간략한 정보를 나타낼 컴포넌트입니다.
- ./src/pages/map/components/InfoRental.js
import React from "react";
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import palette from "../../../styles/palette";
const InfoRental = () => {
const navigation = useNavigation();
const onRental = state => {
// navigation.navigate("Rental", {
// name: "Rental",
// data: state.map
// })
};
return(
<View style={ styles.container } >
<TouchableOpacity style={ styles.rental_button }
onPress={ onRental }
>
<Text style={ styles.rental_text } >
대관하기
</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
width: '100%',
height: '15%',
marginTop: 13,
paddingRight: 10
},
rental_button: {
alignItems: 'center',
justifyContent: 'center',
width: 100,
height: 30,
borderColor: palette.blue[4],
borderWidth: 3,
borderRadius: 30
},
rental_text: {
fontWeight: 'bold'
}
});
export default InfoRental;
대관하기 버튼을 위한 컴포넌트입니다.
- ./src/pages/map/components/MapFooter.js
import React from "react";
import { StyleSheet, View } from "react-native";
import palette from "../../../styles/palette";
import InfoClose from "./InfoClose";
import Info from "./Info";
import InfoRental from "./InfoRental";
const MapFooter = () => {
return(
<View style={ styles.container }>
<InfoClose />
<Info />
<InfoRental />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
height: '30%',
backgroundColor: palette.white[0],
}
});
export default MapFooter;
인포그래픽들을 한 데 모은 컴포넌트입니다.
- ./src/pages/map/MapScreen.js
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { useSelector } from 'react-redux';
import MapFooter from './components/MapFooter';
import MapHeader from './components/MapHeader';
import NaverMap from './components/NaverMap';
const MapScreen = () => {
const { visible } = useSelector(({ marker }) => ({ visible: marker.visible }));
return(
<View style={ styles.container }>
<MapHeader />
<NaverMap />
{
visible &&
<MapFooter />
}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
flex: 1,
},
});
export default MapScreen;
useSelector를 이용하여 redux state값들에 접근하고, marker의 값을 가져와 MapFooter를 보여줄 것인지를 판단하도록 합니다. 그러면 테스트를 진행하겠습니다.
#4 테스트
맵에 마커가 잘 뜨는 모습을 확인할 수 있습니다.
마커 클릭 시 인포그래픽이 잘 생성되고, 닫기 버튼 또한 잘 작동하는 모습을 볼 수 있습니다.
다음 포스트에서는 map 전체 데이터를 가져와 맵에 데이터를 띄우고, 대관하기 버튼을 위한 상세페이지도 만들도록 하겠습니다.
Author And Source
이 문제에 관하여(LifeSports Application(ReactNative & Nest.js) - 14. API gateway 연동, map-service(2)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@biuea/LifeSports-ApplicationReactNative-Nest.js-14.-API-gateway-연동-map-service2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)