Skip to content

Commit f448100

Browse files
committed
Add: redux toolkit
1 parent b48b899 commit f448100

41 files changed

Lines changed: 4232 additions & 4579 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
리뉴얼 강좌 소스 코드는 master 브랜치에 있습니다.
33
리뉴얼 강좌의 ch7는 prepare 폴더입니다.
44

5+
toolkit 적용하고 싶으신 분들을 위해 toolkit 폴더에 소스 코드 정리해두었습니다.
6+
Credits to [소라연](https://github.com/sorayeon/react-nodebird-toolkit)
7+
58
버그가 있을 시 인프런이나 깃헙 issue로 남겨주시면 빠르게 해결하겠습니다.
69

710
[https://nodebird.com](https://nodebird.com)에서 실제 실행 결과물을 확인하실 수 있습니다.
11+

toolkit/front/.babelrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"displayName": true
77
}]
88
]
9-
}
9+
}

toolkit/front/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
"react/forbid-prop-types": "off",
2929
"react/jsx-filename-extension": "off",
3030
"react/jsx-one-expression-per-line": "off",
31+
"react/jsx-props-no-spreading": "off",
3132
"object-curly-newline": "off",
3233
"linebreak-style": "off",
3334
"no-param-reassign": "off"
3435
}
35-
}
36+
}

toolkit/front/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules
2+
/.next
3+
/.idea
4+
react-nodebird.pem

toolkit/front/actions/post.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import axios from 'axios';
2+
import { createAsyncThunk } from '@reduxjs/toolkit';
3+
import { backendUrl } from '../config/config';
4+
import userSlice from '../reducers/user';
5+
6+
axios.defaults.baseURL = backendUrl;
7+
axios.defaults.withCredentials = true; // front, backend 간 쿠키공유
8+
9+
export const loadPosts = createAsyncThunk('post/loadPosts', async (data) => {
10+
const response = await axios.get(`/posts?last=${data?.lastId || 0}`);
11+
return response.data;
12+
});
13+
14+
export const addPost = createAsyncThunk('post/addPost', async (data, thunkAPI) => {
15+
try {
16+
const response = await axios.post('/post', data);
17+
thunkAPI.dispatch(userSlice.actions.addPostToMe(response.data.id));
18+
return response.data;
19+
} catch (error) {
20+
return thunkAPI.rejectWithValue(error.response.data);
21+
}
22+
});
23+
24+
export const uploadImages = createAsyncThunk('post/uploadImages', async (data, { rejectWithValue }) => {
25+
try {
26+
const response = await axios.post('/post/images', data); // POST /post/images
27+
return response.data;
28+
} catch (error) {
29+
return rejectWithValue(error.response.data);
30+
}
31+
});
32+
33+
export const addComment = createAsyncThunk('post/addComment', async (data, { rejectWithValue }) => {
34+
try {
35+
const response = await axios.post(`/post/${data.postId}/comment`, data); // POST /post/1/comment
36+
return response.data;
37+
} catch (error) {
38+
return rejectWithValue(error.response.data);
39+
}
40+
});
41+
42+
export const removePost = createAsyncThunk('post/removePost', async (data, thunkAPI) => {
43+
try {
44+
const response = await axios.delete(`/post/${data.postId}`); // DELETE /post/1/comment
45+
thunkAPI.dispatch(userSlice.actions.removePostToMe(response.data.id));
46+
return response.data;
47+
} catch (error) {
48+
return thunkAPI.rejectWithValue(error.response.data);
49+
}
50+
});
51+
52+
export const loadPost = createAsyncThunk('post/loadPost', async (data, { rejectWithValue }) => {
53+
try {
54+
const response = await axios.get(`/post/${data.postId}`);
55+
return response.data;
56+
} catch (error) {
57+
return rejectWithValue(error.response.data);
58+
}
59+
});
60+
61+
export const likePost = createAsyncThunk('post/likePost', async (data, { rejectWithValue }) => {
62+
try {
63+
const response = await axios.patch(`/post/${data.postId}/like`); // PATCH /post/1/like
64+
return response.data;
65+
} catch (error) {
66+
return rejectWithValue(error.response.data);
67+
}
68+
});
69+
70+
export const unlikePost = createAsyncThunk('post/unlikePost', async (data, { rejectWithValue }) => {
71+
try {
72+
const response = await axios.delete(`/post/${data.postId}/like`); // DELETE /post/1/like
73+
return response.data;
74+
} catch (error) {
75+
return rejectWithValue(error.response.data);
76+
}
77+
});
78+
79+
export const retweet = createAsyncThunk('post/retweet', async (data, { rejectWithValue }) => {
80+
try {
81+
const response = await axios.post(`/post/${data.postId}/retweet`, data);
82+
return response.data;
83+
} catch (error) {
84+
return rejectWithValue(error.response.data);
85+
}
86+
});
87+
88+
export const updatePost = createAsyncThunk('post/updatePost', async (data, { rejectWithValue }) => {
89+
try {
90+
const response = await axios.patch(`/post/${data.postId}`, data);
91+
return response.data;
92+
} catch (error) {
93+
return rejectWithValue(error.response.data);
94+
}
95+
});
96+
97+
export const loadHashtagPosts = createAsyncThunk('post/loadHashtagPosts', async (data, { rejectWithValue }) => {
98+
try {
99+
const response = await axios.get(`/hashtag/${encodeURIComponent(data.hashtag)}?last=${data?.lastId || 0}`);
100+
return response.data;
101+
} catch (error) {
102+
return rejectWithValue(error.response.data);
103+
}
104+
});
105+
106+
export const loadUserPosts = createAsyncThunk('user/loadUserPosts', async (data, { rejectWithValue }) => {
107+
try {
108+
const response = await axios.get(`/user/${data.userId}/posts?last=${data?.lastId || 0}`);
109+
return response.data;
110+
} catch (error) {
111+
return rejectWithValue(error.response.data);
112+
}
113+
});

toolkit/front/actions/user.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import axios from 'axios';
2+
import { createAsyncThunk } from '@reduxjs/toolkit';
3+
import { backendUrl } from '../config/config';
4+
5+
axios.defaults.baseURL = backendUrl;
6+
axios.defaults.withCredentials = true; // front, backend 간 쿠키공유
7+
8+
export const loadMyInfo = createAsyncThunk('user/loadMyInfo', async () => {
9+
const response = await axios.get('/user');
10+
return response.data;
11+
});
12+
13+
export const loadUser = createAsyncThunk('user/loadUser', async (data, { rejectWithValue }) => {
14+
try {
15+
const response = await axios.get(`/user/${data.userId}`);
16+
return response.data;
17+
} catch (error) {
18+
return rejectWithValue(error.response.data);
19+
}
20+
});
21+
22+
export const login = createAsyncThunk('user/login', async (data, { rejectWithValue }) => {
23+
try {
24+
const response = await axios.post('/user/login', data);
25+
return response.data;
26+
} catch (error) {
27+
return rejectWithValue(error.response.data);
28+
}
29+
});
30+
31+
export const logout = createAsyncThunk('user/logout', async () => {
32+
const response = await axios.post('/user/logout');
33+
return response.data;
34+
});
35+
36+
export const signup = createAsyncThunk('user/signup', async (data, { rejectWithValue }) => {
37+
try {
38+
const response = await axios.post('/user', data);
39+
return response.data;
40+
} catch (error) {
41+
return rejectWithValue(error.response.data);
42+
}
43+
});
44+
45+
export const changeNickname = createAsyncThunk('user/changeNickname', async (data, { rejectWithValue }) => {
46+
try {
47+
const response = await axios.patch('/user/nickname', data);
48+
return response.data;
49+
} catch (error) {
50+
return rejectWithValue(error.response.data);
51+
}
52+
});
53+
54+
export const follow = createAsyncThunk('post/follow', async (data, { rejectWithValue }) => {
55+
try {
56+
const response = await axios.patch(`/user/${data.userId}/following`);
57+
return response.data;
58+
} catch (error) {
59+
return rejectWithValue(error.response.data);
60+
}
61+
});
62+
63+
export const unfollow = createAsyncThunk('post/unfollow', async (data, { rejectWithValue }) => {
64+
try {
65+
const response = await axios.delete(`/user/${data.userId}/following`);
66+
return response.data;
67+
} catch (error) {
68+
return rejectWithValue(error.response.data);
69+
}
70+
});
71+
72+
export const removeFollow = createAsyncThunk('post/removeFollow', async (data, { rejectWithValue }) => {
73+
try {
74+
const response = await axios.delete(`/user/${data.userId}/follower`);
75+
return response.data;
76+
} catch (error) {
77+
return rejectWithValue(error.response.data);
78+
}
79+
});
Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,85 @@
1-
import React, { useCallback } from 'react';
2-
import PropTypes from 'prop-types';
1+
import React, { useState, useCallback } from 'react';
2+
import ProTypes from 'prop-types';
33
import Link from 'next/link';
4-
import { Menu, Input, Row, Col } from 'antd';
5-
import styled from 'styled-components';
4+
import {
5+
Layout, Menu, Input, Row, Col,
6+
} from 'antd';
7+
import styled, { createGlobalStyle } from 'styled-components';
68
import { useSelector } from 'react-redux';
7-
import Router from 'next/router';
8-
9+
import Router, { useRouter } from 'next/router';
910
import UserProfile from './UserProfile';
1011
import LoginForm from './LoginForm';
11-
import useInput from '../hooks/useInput';
1212

13+
const { Header, Content } = Layout;
14+
const Global = createGlobalStyle`
15+
.ant-row {
16+
margin-left: 0 !important;
17+
margin-right: 0 !important;
18+
}
19+
.ant-col:first-child {
20+
margin-left: 0 !important;
21+
}
22+
.ant-col:last-child {
23+
margin-right: 0 !important;
24+
}
25+
.ant-form-item-explain-error {
26+
font-size: 11px;
27+
}
28+
`;
1329
const SearchInput = styled(Input.Search)`
1430
vertical-align: middle;
1531
`;
16-
1732
const AppLayout = ({ children }) => {
18-
const [searchInput, onChangeSearchInput] = useInput('');
1933
const { me } = useSelector((state) => state.user);
20-
34+
const [searchInput, setSearchInput] = useState('');
35+
const onChangeSearchInput = useCallback((e) => {
36+
setSearchInput(e.target.value);
37+
}, [searchInput]);
2138
const onSearch = useCallback(() => {
2239
Router.push(`/hashtag/${searchInput}`);
2340
}, [searchInput]);
41+
const router = useRouter();
2442

2543
return (
26-
<div>
27-
<Menu mode="horizontal">
28-
<Menu.Item>
29-
<Link href="/"><a>노드버드</a></Link>
30-
</Menu.Item>
31-
<Menu.Item>
32-
<Link href="/profile"><a>프로필</a></Link>
33-
</Menu.Item>
34-
<Menu.Item>
35-
<SearchInput
36-
enterButton
37-
value={searchInput}
38-
onChange={onChangeSearchInput}
39-
onSearch={onSearch}
40-
/>
41-
</Menu.Item>
42-
</Menu>
43-
<Row gutter={8}>
44-
<Col xs={24} md={6}>
45-
{me ? <UserProfile /> : <LoginForm />}
46-
</Col>
47-
<Col xs={24} md={12}>
48-
{children}
49-
</Col>
50-
<Col xs={24} md={6}>
51-
<a href="https://www.zerocho.com" target="_blank" rel="noreferrer noopener">Made by ZeroCho</a>
52-
</Col>
53-
</Row>
54-
</div>
44+
<Layout className="layout">
45+
<Global />
46+
<Header style={{ position: 'fixed', zIndex: 1, width: '100%' }}>
47+
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={[router.pathname]}>
48+
<Menu.Item key="/">
49+
<Link href="/"><a>노드버드</a></Link>
50+
</Menu.Item>
51+
<Menu.Item key="/profile">
52+
<Link href="/profile"><a>프로필</a></Link>
53+
</Menu.Item>
54+
<Menu.Item key="/search">
55+
<SearchInput
56+
enterButton
57+
value={searchInput}
58+
onChange={onChangeSearchInput}
59+
onSearch={onSearch}
60+
/>
61+
</Menu.Item>
62+
</Menu>
63+
</Header>
64+
<Content style={{ padding: '0 50px', marginTop: 64 }}>
65+
<div style={{ minHeight: '400px', padding: '24px', backgroundColor: '#FFF' }}>
66+
{/* gutter 컬럼 사이의 간격 */}
67+
<Row gutter={12}>
68+
<Col xs={24} sm={24} md={8} lg={4} style={{ paddingTop: '12px' }}>
69+
{me ? <UserProfile /> : <LoginForm />}
70+
</Col>
71+
<Col xs={24} sm={24} md={16} lg={20} style={{ paddingTop: '12px' }}>
72+
{children}
73+
</Col>
74+
</Row>
75+
</div>
76+
</Content>
77+
</Layout>
5578
);
5679
};
5780

5881
AppLayout.propTypes = {
59-
children: PropTypes.node.isRequired,
82+
children: ProTypes.node.isRequired,
6083
};
6184

6285
export default AppLayout;

0 commit comments

Comments
 (0)