LifeSports Application(ReactNative & Nest.js) - 21. post-service(2)
#1 리덕스 모듈
post-service에서 작성한 endpoint들을 위한 api를 작성하겠습니다.
- ./src/lib/api/post.js
import client from './client';
export const write = ({
type,
title,
content,
userId,
writer,
rental
}) => client.post(`http://10.0.2.2:8000/post-service/`, {
type,
title,
content,
userId,
writer,
rental
});
export const getAll = () => client.get(`http://10.0.2.2:8000/post-service/`);
export const getPostsByType = type => client.get(`http://10.0.2.2:8000/post-service/posts/type/${type}`);
export const getOne = _id => client.get(`http://10.0.2.2:8000/post-service/${_id}/post`);
export const getPostsByUserId = userId => client.get(`http://10.0.2.2:8000/post-service/${userId}/posts`);
export const getPostsByKeyword = keyword => client.get(`http://10.0.2.2:8000/post-service/posts/keyword/${keyword}`);
export const deleteOne = _id => client.delete(`http://10.0.2.2:8000/post-service/${id}/post}`);
post 작성과 삭제를 위한 리덕스 모듈을 작성하겠습니다.
- ./src/modules/post.js
import { createAction, handleActions } from "redux-actions";
import createRequestSaga, { createRequestActionTypes } from "../lib/createRequestSaga";
import * as postAPI from '../lib/api/post';
import { takeLatest } from "@redux-saga/core/effects";
const INITIALIZE = 'post/INITIALIZE';
const CHANGE_FIELD = 'post/CHANGE_FIELD';
const [
WRITE,
WRITE_SUCCESS,
WRITE_FAILURE
] = createRequestActionTypes('post/WRITE');
const [
DELETE,
DELETE_SUCCESS,
DELETE_FAILURE
] = createRequestActionTypes('post/DELETE');
export const initialize = createAction(INITIALIZE);
export const changeField = createAction(CHANGE_FIELD, ({
key,
value
}) => ({
key,
value
}));
export const writePost = createAction(WRITE, ({
type,
title,
content,
userId,
writer,
rental
}) => ({
type,
title,
content,
userId,
writer,
rental
}));
export const deletePost = createAction(DELETE, _id => _id);
const writePostSaga = createRequestSaga(WRITE, postAPI.write);
const deletePostSaga = createRequestSaga(DELETE, postAPI.deleteOne);
export function* postSaga() {
yield takeLatest(WRITE, writePostSaga);
yield takeLatest(DELETE, deletePostSaga);
};
const initialState = {
type: null,
title: null,
content: null,
userId: null,
writer: null,
rental: '',
post: null,
postError: null,
message: null,
};
const post = handleActions(
{
[INITIALIZE]: state => initialState,
[CHANGE_FIELD]: (state, { payload: { key, value }}) => ({
...state,
[key]: value,
}),
[WRITE_SUCCESS]: (state, { payload: post }) => ({
...state,
post,
}),
[WRITE_FAILURE]: (state, { payload: postError }) => ({
...state,
postError,
}),
[DELETE_SUCCESS]: (state, { payload: message }) => ({
...state,
message,
}),
[DELETE_FAILURE]: (state, { payload: postError }) => ({
...state,
postError
}),
},
initialState,
);
export default post;
이어서 post를 읽어오는 리덕스 모듈을 작성하겠습니다.
- ./src/modules/posts.js
import { createAction, handleActions } from "redux-actions";
import createRequestSaga, { createRequestActionTypes } from "../lib/createRequestSaga";
import * as postAPI from '../lib/api/post';
import { takeLatest } from "@redux-saga/core/effects";
const INITIALIZE = 'posts/INITIALIZE';
const [
READ_POST,
READ_POST_SUCCESS,
READ_POST_FAILURE
] = createRequestActionTypes('posts/READ_POST');
const [
LIST_ALL,
LIST_ALL_SUCCESS,
LIST_ALL_FAILURE
] = createRequestActionTypes('posts/LIST_ALL');
const [
LIST_POSTS_TYPE,
LIST_POSTS_TYPE_SUCCESS,
LIST_POSTS_TYPE_FAILURE
] = createRequestActionTypes('posts/LIST_POST_TYPES')
const [
LIST_POSTS_USERID,
LIST_POSTS_USERID_SUCCESS,
LIST_POSTS_USERID_FAILURE
] = createRequestActionTypes('posts/LIST_POSTS_USERID');
const [
LIST_POSTS_KEYWORD,
LIST_POSTS_KEYWORD_SUCCESS,
LIST_POSTS_KEYWORD_FAILURE
] = createRequestActionTypes('posts/LIST_POSTS_KEYWORD');
export const readPost = createAction(READ_POST, _id => _id);
export const listAll = createAction(LIST_ALL);
export const listType = createAction(LIST_POSTS_TYPE, type => type);
export const listUserId = createAction(LIST_POSTS_USERID, userId => userId);
export const listKeyword = createAction(LIST_POSTS_KEYWORD, keyword => keyword);
const readPostSaga = createRequestSaga(READ_POST, postAPI.getOne);
const listAllSaga = createRequestSaga(LIST_ALL, postAPI.getAll);
const listTypeSaga = createRequestSaga(LIST_POSTS_TYPE, postAPI.getPostsByType);
const listUserIdSaga = createRequestSaga(LIST_POSTS_USERID, postAPI.getPostsByUserId);
const listKeywordSaga = createRequestSaga(LIST_POSTS_KEYWORD, postAPI.getPostsByKeyword);
export function* postsSaga() {
yield takeLatest(READ_POST, readPostSaga);
yield takeLatest(LIST_ALL, listAllSaga);
yield takeLatest(LIST_POSTS_TYPE, listTypeSaga);
yield takeLatest(LIST_POSTS_USERID, listUserIdSaga);
yield takeLatest(LIST_POSTS_KEYWORD, listKeywordSaga);
};
const initialState = {
post: null,
posts: null,
postsError: null,
};
const posts = handleActions(
{
[INITIALIZE]: state => initialState,
[READ_POST_SUCCESS]: (state, { payload: post }) => ({
...state,
post,
}),
[READ_POST_FAILURE]: (state, { payload: postsError }) => ({
...state,
postsError,
}),
[LIST_ALL_SUCCESS]: (state, { payload: posts }) => ({
...state,
posts,
}),
[LIST_ALL_FAILURE]: (state, { payload: postsError }) => ({
...state,
postsError,
}),
[LIST_POSTS_TYPE_SUCCESS]: (state, { payload: posts }) => ({
...state,
posts,
}),
[LIST_POSTS_TYPE_FAILURE]: (state, { payload: postsError }) => ({
...state,
postsError
}),
[LIST_POSTS_USERID_SUCCESS]: (state, { payload: posts }) => ({
...state,
posts,
}),
[LIST_POSTS_USERID_FAILURE]: (state, { payload: postsError }) => ({
...state,
postsError
}),
[LIST_POSTS_KEYWORD_SUCCESS]: (state, { payload: posts }) => ({
...state,
posts,
}),
[LIST_POSTS_KEYWORD_FAILURE]: (state, { payload: postsError }) => ({
...state,
postsError
}),
},
initialState,
);
export default posts;
모듈들을 완성했으니 UI를 작성하면서 사용을 해보도록 하겠습니다.
#2 UI
react native에서의 UI를 작성해보도록 하겠습니다.
다음의 라이브러리를 설치하겠습니다.
npm install --save react-native-redio-buttons-ext
- ./src/pages/post/components/WriteNav.js
import React from 'react';
import { useDispatch } from 'react-redux';
import { StyleSheet, Text, View } from 'react-native';
import { SegmentedControls } from 'react-native-radio-buttons-ext';
import { changeField } from '../../../modules/post';
const WriteNav = () => {
const dispatch = useDispatch();
const values = [
'함께해요',
'도와주세요'
];
const onSelect = e => {
dispatch(changeField({
key: 'type',
value: e
}));
};
return(
<View style={ styles.container }>
<View style={ styles.label }>
<Text style={ styles.font }>
게시글 종류를 정해주세요
</Text>
</View>
<View style={ styles.radio }>
<SegmentedControls options={ values }
onSelection={ onSelect }
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: 370,
justifyContent: 'flex-start',
marginLeft: 20,
marginTop: 40,
},
label: {
marginBottom: 10,
marginLeft: 10,
},
font: {
fontWeight: 'bold'
},
radio: {
width: 200,
marginBottom: 20
}
});
export default WriteNav;
WriteNav 컴포넌트는 게시글의 종류('함께해요', '도와주세요')를 정하는 역할을 합니다.
- ./src/pages/post/components/WriteContent.js
import React from 'react';
import { StyleSheet, Text, TextInput, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { changeField } from '../../../modules/post';
import palette from '../../../styles/palette';
import RentalList from './RentalList';
const WriteContent = () => {
const dispatch = useDispatch();
const { type } = useSelector(({ post }) => ({ type: post.type }));
const onChange = e => {
const inputAccessoryViewID = e.target._internalFiberInstanceHandleDEV.memoizedProps.inputAccessoryViewID;
const value = e.nativeEvent.text;
dispatch(changeField({
key: inputAccessoryViewID,
value,
}));
};
return(
<View>
<View style={ styles.container }>
<View style={ styles.label }>
<Text style={ styles.font }>
제목을 적어주세요
</Text>
</View>
<TextInput style={ styles.title_input }
inputAccessoryViewID="title"
onChange={ onChange }
/>
</View>
<View style={ styles.container }>
<View style={ styles.label }>
<Text style={ styles.font }>
내용을 적어주세요
</Text>
</View>
<TextInput style={ styles.content_input }
inputAccessoryViewID="content"
multiline={ true }
textAlignVertical="top"
onChange={ onChange }
/>
</View>
{
type === '함께해요' &&
<View style={ styles.container }>
<View style={ styles.label }>
<Text style={ styles.font }>
대관 내역이에요
</Text>
</View>
<RentalList />
</View>
}
</View>
);
};
const styles = StyleSheet.create({
container: {
width: 370,
justifyContent: 'flex-start',
marginLeft: 20,
marginTop: 40,
borderBottomColor: palette.gray[3],
borderBottomWidth: 2,
},
label: {
marginBottom: 10,
marginLeft: 10,
},
font: {
fontWeight: 'bold'
},
title_input: {
width: 370,
height: 40,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 6,
backgroundColor: palette.white[0],
color: palette.black[0]
},
content_input: {
width: 370,
height: 200,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 6,
backgroundColor: palette.white[0],
color: palette.black[0]
}
});
export default WriteContent;
해당 컴포넌트에서는 제목, 내용을 작성할 수 있습니다. 그리고 최상단에서의 게시글 타입에 따라 함께해요의 경우엔 현재 대관한 내역을 게시글에 담을 수 있으며, 도와주세요의 경우에는 대관 내역을 보이지 않게 하도록 설정하였습니다.
- ./src/pages/post/components/RentalList.js
import React, { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { getRentals } from '../../../modules/rentals';
import palette from '../../../styles/palette';
import RentalCard from './RentalCard';
const RentalList = () => {
const dispatch = useDispatch();
const {
userId,
rentals,
} = useSelector(({
user,
rentals
}) => ({
userId: user.user.userId,
rentals: rentals.rentals,
}));
useEffect(() => {
dispatch(getRentals(userId))
}, [dispatch, userId]);
return(
<View style={ styles.container }>
{
rentals ?
rentals.map((item, i) => {
return <RentalCard item={ item }/>
}) :
<Text>
대관 내역이 없어요!
</Text>
}
</View>
);
};
const styles = StyleSheet.create({
container: {
width: 370,
justifyContent: 'center',
}
});
export default RentalList;
대관 내역 리스트입니다. rentals 리덕스 모듈을 활용하여 getRentals를 호출해서 현재 사용자가 대관한 리스트를 불러올 수 있게 하는 컴포넌트입니다.
- ./src/pages/post/components/RentalCard.js
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { changeField } from '../../../modules/post';
import palette from '../../../styles/palette';
const RentalCard = ({ item }) => {
const dispatch = useDispatch();
const { rental } = useSelector(({ post }) => ({ rental: post.rental }));
const onChange = e => {
dispatch(changeField({
key: 'rental',
value: item
}));
};
return (
<View style={
rental.rentalId !== item.rentalId ?
styles.card :
styles.select_card
}>
<TouchableOpacity onPress={ onChange }>
<View>
<Text style={
rental.rentalId === item.rentalId &&
styles.select_font
}>
{ item.mapName }
</Text>
</View>
<View>
<Text style={
rental.rentalId === item.rentalId &&
styles.select_font
}>
{ item.date }
</Text>
</View>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
card: {
width: 350,
height: 70,
backgroundColor: palette.gray[2],
margin: 10,
padding: 15,
borderRadius: 10
},
select_card: {
width: 350,
height: 70,
backgroundColor: palette.blue[1],
margin: 10,
padding: 15,
borderRadius: 10
},
select_font: {
color: palette.white[0]
}
});
export default RentalCard;
각 대관 데이터를 클릭하게 되면 post.rental state에 담길 수 있도록 하는 컴포넌트입니다. 선택된 데이터는 파란색으로 표시가 됩니다.
- ./src/pages/post/components/WriteFooter.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import WriteButton from './WriteButton';
const WriteFooter = () => {
return(
<View style={ styles.container }>
<WriteButton />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: 370,
alignItems: 'flex-end',
margin: 20,
},
});
export default WriteFooter;
맨 하단에서 작성 완료 버튼을 감싸주는 컴포넌트입니다.
- ./src/pages/post/components/WriteButton.js
import React, { useEffect } from 'react';
import { StyleSheet, Text, ToastAndroid, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initialize, writePost } from '../../../modules/post';
import palette from '../../../styles/palette';
const WriteButton = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const {
type,
title,
content,
userId,
writer,
nickname,
rental,
post
} = useSelector(({
user,
post
}) => ({
type: post.type,
title: post.title,
content: post.content,
userId: user.user.userId,
nickname: user.user.nickname,
writer: post.writer,
rental: post.rental,
post: post.post
}));
const showToastForError = e => {
ToastAndroid.show(
"Empty input! check inputs",
ToastAndroid.SHORT
);
};
const onWrite = e => {
if([
type,
title,
content,
userId,
writer,
].includes(null)) {
showToastForError();
return;
}
dispatch(writePost({
type,
title,
content,
userId,
writer,
rental
}));
};
useEffect(() => {
dispatch(changeField({
key: 'userId',
value: userId
}));
}, [dispatch, userId]);
useEffect(() => {
dispatch(changeField({
key: 'writer',
value: nickname
}));
}, [dispatch, nickname]);
useEffect(() => {
if(post) {
dispatch(initialize());
navigation.navigate("Post");
}
}, [dispatch, post]);
return <TouchableOpacity style={ styles.shape }
onPress={ onWrite }
>
<Text style={ styles.font }>
작성 완료
</Text>
</TouchableOpacity>;
};
const styles = StyleSheet.create({
shape: {
width: 120,
height: 40,
backgroundColor: palette.blue[1],
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center'
},
font: {
color: palette.white[0],
fontWeight: 'bold'
}
});
export default WriteButton;
state에 저장된 값을 전부 불러와 작성 완료 버튼을 동작하게 하는 컴포넌트입니다. 게시글의 요소 중 하나라도 비어있으면 showToastForError 메서드가 호출되어 toast 메시지를 보여줍니다. 최종적으로 정상적으로 게시글이 작성 완료가 된다면 게시글 홈으로 이동합니다.
구현이 잘 되었는지 테스트를 진행해보도록 하겠습니다.
#3 테스트
- post-service의 main.ts 파일로 가서 port번호를 7100으로 설정하겠습니다.
konga를 이용하여 post-service를 등록을 마친 후, 다음의 순서대로 테스트를 진행해보도록 하겠습니다.
1) 함께해요 - input이 비어있는 오류
2) 함께해요 - 대관 데이터를 포함한 게시글
3) 함께해요 - 대관 데이터가 포함되지 않은 게시글
2), 3)에 관한 데이터 REST 요청이 잘 진행됨을 알 수 있습니다 데이터베이스 상태도 확인해보도록 하겠습니다.
4) 도와주세요 - input이 비어있는 오류
5) 도와주세요
1) ~ 5)의 테스트를 완료한 결과 정상적으로 수행됨을 볼 수 있습니다. 다음 포스트에서는 게시글 상세페이지를 작업해보도록 하겠습니다.
Author And Source
이 문제에 관하여(LifeSports Application(ReactNative & Nest.js) - 21. post-service(2)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@biuea/LifeSports-ApplicationReactNative-Nest.js-21.-post-service2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)