LifeSports Application(ReactNative & Nest.js) - 14. API Gatewayバインド、map-service(2)

32281 ワード

#1クライアント、API Gatewayのバインド


前回の記事でKongを用いてapiゲートウェイを実現した.そして、このAPIサーバとREACTネイティブクライアントを1つのポートとして複数のサービスとの通信を実現します.
reactネイティブパッケージ.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');
    tokenフィールドをauthサービスの応答ユーザーに追加し、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、wayfing-serviceのバックエンドが完了し、クライアントにmapに関連する冗長モジュールを作成させます.

    #3marker,mapredux


    mapのmarkerは、検索するデータがどこにあるかを示す高可視性の要素です.管理アプリケーションでmarkerは、次の機能を実行します.
    1)タグをクリックすると、下部に場所に関する模式図が作成されます.
    2)情報図は場所情報を含み、「拡大」ボタンがある.
    3)「拡大」ボタンをクリックすると、詳細、拡大関連機能を実行できます.
    上記の機能を実現するには、まず人口図形を作成します.したがって、タグをクリックすると、冗長に作成する表示フラグのステータス値にtrueとfalseが追加され、trueの写真の高さが30%になり、マッピングのサイズが60%になります.
    また、mapでタグに使用されるマッピングデータはリスト全体にインポートされ、タグごとにデータが付与されるため、map redoxを実装してmap情報を含む.
    まずmarkerを作成し、infoグラフィックの作成から実装します.
  • ./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値をredus state値に設定してmarkerをクリックすると、visibleでtrueをクリックし、ボタンを閉じるときにfalse値を与えます.次に、マッピング中にタグに存在するマッピングデータに変更します.
  • ./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が完了した以上、infoグラフィックを実装し、markerをクリックすると良好なinfoグラフィックが生成されるかどうかをテストします.
  • ./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;
    [拡張](Extend)ボタンに使用される構成部品.
  • ./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;
    userSelectorを使用してredox state値にアクセスし、marker値を取得してMapFooterを表示するかどうかを決定します.ではテストを行いましょう

    #4テスト



    総覧図にマークされた表示効果が表示されます.


    タグをクリックすると、良好な入力グラフィックが作成され、ボタンを閉じると正常に動作します.
    次の記事では、map全体のデータをインポートし、mapにデータを表示し、[スケール](Scale)ボタンを表示する詳細なページを作成します.