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 전체 데이터를 가져와 맵에 데이터를 띄우고, 대관하기 버튼을 위한 상세페이지도 만들도록 하겠습니다.

좋은 웹페이지 즐겨찾기