전 포스트에서 게시글에 관한 전반적인 리덕스 모듈을 완성시켰으니 게시글 홈에서 게시글 데이터를 불러오도록 하겠습니다.
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로 설정하겠습니다.
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라는 훅은 네비게이션으로 이동이 될 때마다 리렌더링 해주는 훅입니다.
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을 이용하여 게시글을 클릭하면 해당 게시글의 상세페이지로 이동합니다.
게시글 홈이 완성되었으니 화면을 확인해보도록 하겠습니다.
잘 출력되는 모습을 볼 수 있습니다.
이어서 상세페이지 UI를 만들어 모듈을 호출하고 사용해보도록 하겠습니다.
카카오 오븐으로 작성해 본 디자인을 바탕으로 제작을 해보도록 하겠습니다.
PostStackNavigation에 PostDetailScreen을 등록하도록 하겠습니다.
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;
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 데이터를 넘겨줍니다.
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;
부모 컴포넌트에서 넘겨받은 게시글 데이터를 각 컴포넌트에 뿌려주는 역할을 합니다.
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;
제목, 게시글 생성 날짜, 작성자를 보여주는 컴포넌트입니다.
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에서 구현하도록 하겠습니다.
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;
게시글의 내용을 볼 수 있는 컴포넌트입니다.
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가 완성되었으니 화면을 보도록 하겠습니다.
화면이 잘 출력되는 모습을 볼 수 있습니다.
대관 위치 정보는 네이버 맵 api를 이용하여 맵 컴포넌트에 대관 위치를 보여줍니다.
...
export const getOne = _id => client.get(`http://10.0.2.2:8000/map-service/map/${_id}`);
...
리덕스 모듈을 작성해보겠습니다.
...
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;
한 개의 맵 데이터를 불러올 수 있도록 모듈에 코드를 추가했습니다.
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
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에 완성된 컴포넌트를 등록하고 테스트를 진행해보겠습니다.
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에 추가해보도록 하겠습니다.