LifeSports Application(ReactNative & Nest.js) - 22. post-service(3)

yellow_note·2021년 10월 22일
0

#1 게시글 홈 UI

전 포스트에서 게시글에 관한 전반적인 리덕스 모듈을 완성시켰으니 게시글 홈에서 게시글 데이터를 불러오도록 하겠습니다.

  • ./src/pages/post/PostScreen.js
import * as React from 'react';
import { StyleSheet, Text, ScrollView } from 'react-native';
import palette from '../../styles/palette';
import PostContent from './components/PostContent';
import PostHeader from './components/PostHeader';
import PostNav from './components/PostNav';

const PostScreen = () => {
    return(
        <ScrollView style={ styles.container }>
            <PostHeader />
            <PostNav />
            <PostContent />
        </ScrollView>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.gray[2],
    },
});

export default PostScreen;

우선 전체 배경색을 gray로 설정하겠습니다.

  • ./src/pages/post/components/PostContent.js
import React, { useEffect } from 'react';
import {
    StyleSheet,
    View
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { listAll } from '../../../modules/posts';
import palette from '../../../styles/palette';
import Loading from '../../../styles/common/Loading';
import PostCard from './PostCard';
import { useIsFocused, useNavigation } from '@react-navigation/core';
import { TouchableOpacity } from 'react-native-gesture-handler';

const PostContent = () => {
    const dispatch = useDispatch();
    const isFocused = useIsFocused();
    const { posts } = useSelector(({ posts }) => ({ posts: posts.posts }));

    useEffect(() => {
        dispatch(listAll());
    }, [dispatch, isFocused]);

    return(
        <View style={ styles.container }>
            {
                posts ?
                posts.map((item, i) => {
                    return <TouchableOpacity onPress={ toDetailPage }>
                               <PostCard item={ item } />
                           </TouchableOpacity>
                }) : <Loading />
            }
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.gray[2],
    },
});

export default PostContent;

listAll메서드를 호출해 전체 게시글 데이터를 불러옵니다. 여기서 isFocused라는 훅은 네비게이션으로 이동이 될 때마다 리렌더링 해주는 훅입니다.

  • ./src/pages/post/components/PostCard.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import palette from '../../../styles/palette';
import Icon from 'react-native-vector-icons/Ionicons';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/native';

const PostCard = ({ item }) => {
    const navigation = useNavigation();
    const toDetailPage = e => {
        navigation.navigate("PostDetail", {
            _id: item._id
        });
    };

    var info = 
    (item.rental && item.rental.status) === "BEING" ? 
    <View style={ styles.info }>
        <Icon size={ 30 } 
            name={ "ios-checkmark-sharp" }
            color={ palette.blue[2] }
        />
        <Text>대관이 되어있어요!</Text>
    </View> : null;

    return(
        <View style={ styles.container }>
            <TouchableOpacity onPress={ toDetailPage }>
                <View style={ styles.type_info }>
                    <Text style={ styles.type }>
                        { item.type }
                    </Text>
                    { info }
                </View>
                <View style={ styles.row }>
                    <Text>
                        { item.title }
                    </Text>
                </View>
            </TouchableOpacity>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.white[0],
        borderRadius: 6,
        margin: 10,
        padding: 10
    },
    row: {
        marginTop: 10,
    },
    type: {
        fontWeight: 'bold',
        fontSize: 15
    },
    info: {
        alignItems: 'center',
        flexDirection: 'row',
        marginLeft: 170
    },
    type_info: {
        flexDirection: 'row',
        alignItems: 'center',
    }
});

export default PostCard;

info라는 변수는 조건부 렌더링 시 사용되는 변수입니다. 게시글 데이터를 읽어와 rental의 상태값이 BEING이어야만 대관이 되어있다는 메시지를 카드에 담을 수 있게 하였습니다. 그리고 navigation을 이용하여 게시글을 클릭하면 해당 게시글의 상세페이지로 이동합니다.

게시글 홈이 완성되었으니 화면을 확인해보도록 하겠습니다.

잘 출력되는 모습을 볼 수 있습니다.

#2 상세페이지 UI

이어서 상세페이지 UI를 만들어 모듈을 호출하고 사용해보도록 하겠습니다.

카카오 오븐으로 작성해 본 디자인을 바탕으로 제작을 해보도록 하겠습니다.

PostStackNavigation에 PostDetailScreen을 등록하도록 하겠습니다.

  • ./src/navigator/post/PostStackNavigation.js
import 'react-native-gesture-handler';
import * as React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import PostScreen from '../../pages/post/PostScreen';
import PostWriteScreen from '../../pages/post/PostWriteScreen';
import PostDetailScreen from '../../pages/post/PostDetailScreen';

const Stack = createStackNavigator();

const PostStackNavigation = () => {
    return(
        <Stack.Navigator>
            <Stack.Screen name="Post"
                          component={ PostScreen }
                          options={{
                              headerShown: false,
                          }}
            />
            <Stack.Screen name="PostWrite"
                          component={ PostWriteScreen }
            />
            <Stack.Screen name="PostDetail" 
                          component={ PostDetailScreen }
            />
        </Stack.Navigator>
    );
};

export default PostStackNavigation;
  • ./src/pages/post/PostDetailScreen.js
import { useRoute } from '@react-navigation/core';
import React, { useEffect } from 'react';
import { StyleSheet, ScrollView } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { readPost } from '../../modules/posts';
import palette from '../../styles/palette';
import Loading from '../../styles/common/Loading';
import DetailFragment from './components/DetailFragment';

const PostDetailScreen = () => {
    const route = useRoute();
    const dispatch = useDispatch();
    const { post } = useSelector(({ posts }) => ({ post: posts.post }));

    useEffect(() => {
        dispatch(readPost(route.params._id));
    }, [dispatch, route]);

    return(
        <ScrollView style={ styles.container }>
            {
                post ?
                <DetailFragment item={ post }/> : <Loading />
            }
        </ScrollView>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.gray[2] 
    }
})

export default PostDetailScreen;

id값을 읽어와 게시글 데이터를 불러옵니다. 그 후 Fragment에 post 데이터를 넘겨줍니다.

  • ./src/pages/post/components/DetailFragment.js
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import DetailHeader from './DetailHeader';
import DetailArticle from './DetailArticle';
import DetailContent from './DetailContent';
import DetailFooter from './DetailFooter';

const DetailFragment = item => {
    const post = item.item;
    const [info, setInfo] = useState(null);

    console.log(item);
    
    useEffect(() => {
        setInfo(
            (post.rental && post.rental.status) === "BEING" ? 
            <DetailArticle post={ post }/> : 
            null
        );
    }, [post]);

    return(
        <View>
            <DetailHeader post={ post } />
            { info }
            <DetailContent post={ post } />
            <DetailFooter post={ post } />
        </View>
    );
};

export default DetailFragment;

부모 컴포넌트에서 넘겨받은 게시글 데이터를 각 컴포넌트에 뿌려주는 역할을 합니다.

  • ./src/pages/post/components/PostHeader.js
import React from 'react'
import { StyleSheet, Text, View } from 'react-native';
import palette from '../../../styles/palette';

const DetailHeader = post => {
    return(
        <View style={ styles.container }>
            <Text style={ styles.title }>
                { post.post.title }
            </Text>
            <Text>
                { post.post.createdAt.substring(0, 15) }
            </Text>
            <Text>
                { post.post.writer }
            </Text>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.white[0],
        padding: 20
    },
    title: {
        fontSize: 20,
        fontWeight: 'bold'
    }
});

export default DetailHeader;

제목, 게시글 생성 날짜, 작성자를 보여주는 컴포넌트입니다.

  • ./src/pages/post/components/DetailArticle.js
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import palette from '../../../styles/palette';
import Icon from 'react-native-vector-icons/Ionicons';
import { useNavigation } from '@react-navigation/native';

const DetailArticle = post => {
    const navigation = useNavigation()
    // const toMapDetail = e => {
    //     navigation.navigate("MapToWhere", {
    //         _id: post.post.rental.mapId
    //     });
    // };

    return(
        <View style={ styles.container }>
            <View style={ styles.info }>
                <Icon size={ 30 } 
                      name={ "ios-checkmark-sharp" }
                      color={ palette.blue[2] }
                />
                <Text>
                    { post.post.writer } 님이 대관을 하였어요!
                </Text>
            </View>
            <View style={ styles.row }>
                <Text>
                    { post.post.rental.mapName }
                </Text>
                <Text>
                    { post.post.rental.date }
                </Text>
            </View>
            <View style={ styles.footer }>
                <TouchableOpacity>
                    <Text>
                        대관 위치 보러갈래요
                    </Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        margin: 15,
        borderRadius: 15,
        padding: 15,
        backgroundColor: palette.white[0]
    },
    info: {
        alignItems: 'center',
        flexDirection: 'row',
    },
    row: {
        marginTop: 20,
    },
    footer: {
        marginTop: 20,
        alignItems: 'flex-end'
    }
});

export default DetailArticle;

대관 데이터를 보여주는 컴포넌트입니다. 처음 PostDetailScreen 페이지에서 게시글의 대관 데이터 존재 유무에 따라 보여지는 컴포넌트입니다. 이 컴포넌트에서는 대관 위치 정보를 볼 수 있는 기능이 존재하며 #3에서 구현하도록 하겠습니다.

  • ./src/pages/post/components/DetailContent.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import palette from '../../../styles/palette';

const DetailContent = post => {
    return(
        <View style={ styles.container }>
            <Text>
                { post.post.content }
            </Text>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        padding: 15,
        backgroundColor: palette.white[0]
    }
});

export default DetailContent;

게시글의 내용을 볼 수 있는 컴포넌트입니다.

  • ./src/pages/post/components/DetailFooter.js
import React from 'react';
import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import palette from '../../../styles/palette';
import Icon from 'react-native-vector-icons/Ionicons';

const DetailFooter = post => {
    const onChat = e => {

    };

    return(
        <View style={ styles.container }>
            <View style={ styles.search_box }>
                <TextInput style={ styles.input }
                           multiline={ true }
                />
                <TouchableOpacity onPress={ onChat }>
                    <Icon name={ 'ios-paper-plane-outline' }
                          size={ 30 }
                          color={ palette.blue[1] }
                    />
                </TouchableOpacity>
            </View>
            <View style={ styles.comment_box }>
                {/* Add chat */}
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 10,
        backgroundColor: palette.white[0]
    },
    search_box: {
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        height: 50,
        margin: 10,
        borderRadius: 10,
        backgroundColor: palette.gray[1]
    },
    input: {
        flex: 0.9,
        margin: 5,
    },
    comment_box: {
        flex: 1,
        margin: 10,
        backgroundColor: palette.white[0]
    }
});

export default DetailFooter;

게시글의 하단에 존재하는 댓글 관련 컴포넌트입니다. 이 기능은 다음 포스트에서 post-service에 추가적으로 댓글 기능을 구현하면서 완성하도록 하겠습니다.

UI가 완성되었으니 화면을 보도록 하겠습니다.

화면이 잘 출력되는 모습을 볼 수 있습니다.

#3 대관 위치 정보

대관 위치 정보는 네이버 맵 api를 이용하여 맵 컴포넌트에 대관 위치를 보여줍니다.

  • ./src/lib/api/maps.js
...

export const getOne = _id => client.get(`http://10.0.2.2:8000/map-service/map/${_id}`);

...

리덕스 모듈을 작성해보겠습니다.

  • ./src/modules/map.js
...

const [
    GET_MAP,
    GET_MAP_SUCCESS,
    GET_MAP_FAILURE
] = createRequestActionTypes('map/GET_MAP');

...

export const readMap = createAction(GET_MAP, _id => _id);

const readMapSaga = createRequestSaga(GET_MAP, mapAPI.getOne);

export function* mapSaga() {
    yield takeLatest(GET_MAP, readMapSaga);
};

const initialState = {
    map: null,
    error: null,
};

const map = handleActions(
    {
        ...
        [GET_MAP_SUCCESS]: (state, { payload: map }) => ({
            ...state,
            map,
        }),
        [GET_MAP_FAILURE]: (state, { payload: error }) => ({
            ...state,
            error
        }),
    },
    initialState,
);

export default map;

한 개의 맵 데이터를 불러올 수 있도록 모듈에 코드를 추가했습니다.

  • ./src/pages/map/MapToWhereScreen.js
import { useRoute } from '@react-navigation/core';
import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { readMap } from '../../modules/map';
import Loading from '../../styles/common/Loading';
import MapToWhereFragment from './components/MapToWhereFragment';

const MapToWhereScreen = () => {
    const route = useRoute();
    const dispatch = useDispatch();
    const { map } = useSelector(({ map }) => ({ map: map.map }));
    const _id = route.params._id;

    useEffect(() => {
        dispatch(readMap(_id));
    }, [dispatch, _id]);

    return(
        <View style={ styles.container }>
            {
                map ?
                <MapToWhereFragment item={ map }/> : <Loading />
            }
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
});

export default MapToWhereScreen
  • ./src/pages/map/components/MapToWhereFragment.js
import React from 'react';
import { StyleSheet } from 'react-native';
import CustomMarker from './CustomMarker';
import NaverMapView from 'react-native-nmap';

const MapToWhereFragment = item => {
    const map = item.item;
    const defaultLocation = {
        latitude: map.ycode, 
        longitude: map.xcode
    };

    console.log(item);

    return <NaverMapView style={ styles.map }
                         showsMyLocationButton={ true }
                         center={{
                             ...defaultLocation, 
                             zoom: 15 
                         }}
                         scaleBar={ true }
           >
                    {
                        map &&
                        <CustomMarker data={ map } />
                    }
           </NaverMapView>;
};

const styles = StyleSheet.create({
    map: {
        width: '100%',
        height: '90%'
    }
});

export default MapToWhereFragment;

MapToWhereScreen은 이전에 작성해 본 NaverMap.js와 다를 것이 없기 때문에 구현하는데 어려움은 없을 것 같습니다.

PostStackNavigation에 완성된 컴포넌트를 등록하고 테스트를 진행해보겠습니다.

  • ./src/navigator/post/PostStackNavigation.js
import 'react-native-gesture-handler';
import * as React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import PostScreen from '../../pages/post/PostScreen';
import PostWriteScreen from '../../pages/post/PostWriteScreen';
import PostDetailScreen from '../../pages/post/PostDetailScreen';
import MapToWhereScreen from '../../pages/map/MapToWhereScreen';

const Stack = createStackNavigator();

const PostStackNavigation = () => {
    return(
        <Stack.Navigator>
            ...
            <Stack.Screen name="MapToWhere"
                          component={ MapToWhereScreen }
            />
        </Stack.Navigator>
    );
};

export default PostStackNavigation;

잘 작동하는 모습을 볼 수 있습니다.

다음 포스트에서는 댓글 관련 기능을 post-service에 추가해보도록 하겠습니다.

0개의 댓글

관련 채용 정보