= ({ statusCode }) => (
8 |
9 |
10 | {statusCode}
11 | {' '}
12 | 에러 발생
13 |
14 |
15 | );
16 |
17 | MyError.defaultProps = {
18 | statusCode: 400,
19 | };
20 |
21 | MyError.getInitialProps = async (context) => {
22 | const statusCode = context.res ? context.res.statusCode : context.err ? context.err.statusCode : null;
23 | return { statusCode };
24 | };
25 |
26 | export default MyError;
27 |
--------------------------------------------------------------------------------
/ts/front/pages/hashtag/[tag].tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { useRouter } from 'next/router';
5 |
6 | import { LOAD_HASHTAG_POSTS_REQUEST } from '../../reducers/post';
7 | import PostCard from '../../containers/PostCard';
8 |
9 | const Hashtag = ({ tag }) => {
10 | const router = useRouter();
11 | const dispatch = useDispatch();
12 | const { id } = router.query;
13 |
14 | const { mainPosts, hasMorePost } = useSelector((state) => state.post);
15 |
16 | const onScroll = useCallback(() => {
17 | if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
18 | if (hasMorePost) {
19 | dispatch({
20 | type: LOAD_HASHTAG_POSTS_REQUEST,
21 | lastId: mainPosts[mainPosts.length - 1] && mainPosts[mainPosts.length - 1].id,
22 | data: tag,
23 | });
24 | }
25 | }
26 | }, [hasMorePost, mainPosts.length, tag]);
27 |
28 | useEffect(() => {
29 | window.addEventListener('scroll', onScroll);
30 | return () => {
31 | window.removeEventListener('scroll', onScroll);
32 | };
33 | }, [mainPosts.length, hasMorePost, tag]);
34 |
35 | return (
36 |
37 | {mainPosts.map((c) => (
38 |
39 | ))}
40 |
41 | );
42 | };
43 |
44 | Hashtag.propTypes = {
45 | tag: PropTypes.string.isRequired,
46 | };
47 |
48 | Hashtag.getInitialProps = async (context) => {
49 | const { tag } = context.query;
50 | console.log('hashtag getInitialProps', tag);
51 | context.store.dispatch({
52 | type: LOAD_HASHTAG_POSTS_REQUEST,
53 | data: tag,
54 | });
55 | return { tag };
56 | };
57 |
58 | export default Hashtag;
59 |
--------------------------------------------------------------------------------
/ts/front/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useCallback, useRef } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import PostForm from '../containers/PostForm';
4 | import PostCard from '../containers/PostCard';
5 | import { LOAD_MAIN_POSTS_REQUEST } from '../reducers/post';
6 |
7 | const Home = () => {
8 | const { me } = useSelector((state) => state.user);
9 | const { mainPosts, hasMorePost } = useSelector((state) => state.post);
10 | const dispatch = useDispatch();
11 | const countRef = useRef([]);
12 |
13 | const onScroll = useCallback(() => {
14 | if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
15 | if (hasMorePost) {
16 | const lastId = mainPosts[mainPosts.length - 1].id;
17 | if (!countRef.current.includes(lastId)) {
18 | dispatch({
19 | type: LOAD_MAIN_POSTS_REQUEST,
20 | lastId,
21 | });
22 | countRef.current.push(lastId);
23 | }
24 | }
25 | }
26 | }, [hasMorePost, mainPosts.length]);
27 |
28 | useEffect(() => {
29 | window.addEventListener('scroll', onScroll);
30 | return () => {
31 | window.removeEventListener('scroll', onScroll);
32 | countRef.current = [];
33 | };
34 | }, [hasMorePost, mainPosts.length]);
35 |
36 | return (
37 |
38 | {me &&
}
39 | {mainPosts.map((c) => (
40 |
41 | ))}
42 |
43 | );
44 | };
45 |
46 | Home.getInitialProps = async (context) => {
47 | console.log(Object.keys(context));
48 | context.store.dispatch({
49 | type: LOAD_MAIN_POSTS_REQUEST,
50 | });
51 | };
52 |
53 | export default Home;
54 |
--------------------------------------------------------------------------------
/ts/front/pages/post/[id].tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import Helmet from 'react-helmet';
5 | import { useRouter } from 'next/router';
6 |
7 | import { LOAD_POST_REQUEST } from '../../reducers/post';
8 | import { backUrl } from '../../config/config';
9 |
10 | const Post = () => {
11 | const router = useRouter();
12 | const { id } = router.query;
13 | const { singlePost } = useSelector((state) => state.post);
14 | return (
15 | <>
16 |
30 | {singlePost.content}
31 | {singlePost.User.nickname}
32 |
33 | {singlePost.Images[0] &&

}
34 |
35 | >
36 | );
37 | };
38 |
39 | Post.getInitialProps = async (context) => {
40 | context.store.dispatch({
41 | type: LOAD_POST_REQUEST,
42 | data: context.query.id,
43 | });
44 | return { id: parseInt(context.query.id, 10) };
45 | };
46 |
47 | export default Post;
48 |
--------------------------------------------------------------------------------
/ts/front/pages/profile.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import NicknameEditForm from '../containers/NicknameEditForm';
5 | import {
6 | LOAD_FOLLOWERS_REQUEST,
7 | LOAD_FOLLOWINGS_REQUEST,
8 | REMOVE_FOLLOWER_REQUEST,
9 | UNFOLLOW_USER_REQUEST,
10 | } from '../reducers/user';
11 | import { LOAD_USER_POSTS_REQUEST } from '../reducers/post';
12 | import PostCard from '../containers/PostCard';
13 | import FollowList from '../components/FollowList';
14 |
15 | const Profile = () => {
16 | const dispatch = useDispatch();
17 | const { followingList, followerList, hasMoreFollower, hasMoreFollowing } = useSelector((state) => state.user);
18 | const { mainPosts } = useSelector((state) => state.post);
19 |
20 | const onUnfollow = useCallback((userId) => () => {
21 | dispatch({
22 | type: UNFOLLOW_USER_REQUEST,
23 | data: userId,
24 | });
25 | }, []);
26 |
27 | const onRemoveFollower = useCallback((userId) => () => {
28 | dispatch({
29 | type: REMOVE_FOLLOWER_REQUEST,
30 | data: userId,
31 | });
32 | }, []);
33 |
34 | const loadMoreFollowings = useCallback(() => {
35 | dispatch({
36 | type: LOAD_FOLLOWINGS_REQUEST,
37 | offset: followingList.length,
38 | });
39 | }, [followingList.length]);
40 |
41 | const loadMoreFollowers = useCallback(() => {
42 | dispatch({
43 | type: LOAD_FOLLOWERS_REQUEST,
44 | offset: followerList.length,
45 | });
46 | }, [followerList.length]);
47 |
48 | return (
49 |
50 |
51 |
58 |
65 |
66 | {mainPosts.map((c) => (
67 |
68 | ))}
69 |
70 |
71 | );
72 | };
73 |
74 | Profile.getInitialProps = async (context) => {
75 | const state = context.store.getState();
76 | // 이 직전에 LOAD_USERS_REQUEST
77 | context.store.dispatch({
78 | type: LOAD_FOLLOWERS_REQUEST,
79 | data: state.user.me && state.user.me.id,
80 | });
81 | context.store.dispatch({
82 | type: LOAD_FOLLOWINGS_REQUEST,
83 | data: state.user.me && state.user.me.id,
84 | });
85 | context.store.dispatch({
86 | type: LOAD_USER_POSTS_REQUEST,
87 | data: state.user.me && state.user.me.id,
88 | });
89 |
90 | // 이 쯤에서 LOAD_USERS_SUCCESS 돼서 me가 생김.
91 | };
92 |
93 | export default Profile;
94 |
--------------------------------------------------------------------------------
/ts/front/pages/signup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useEffect } from 'react';
2 | import { Button, Checkbox, Form, Input } from 'antd';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import Router from 'next/router';
5 | import styled from 'styled-components';
6 | import { SIGN_UP_REQUEST } from '../reducers/user';
7 | import useInput from '../utils/useInput';
8 |
9 | const SignupError = styled.div`
10 | color: red;
11 | `;
12 |
13 | const Signup = () => {
14 | const [passwordCheck, setPasswordCheck] = useState('');
15 | const [term, setTerm] = useState(false);
16 | const [passwordError, setPasswordError] = useState(false);
17 | const [termError, setTermError] = useState(false);
18 |
19 | const [id, onChangeId] = useInput('');
20 | const [nick, onChangeNick] = useInput('');
21 | const [password, onChangePassword] = useInput('');
22 | const dispatch = useDispatch();
23 | const { isSigningUp, me } = useSelector((state) => state.user);
24 |
25 | useEffect(() => {
26 | if (me) {
27 | alert('로그인했으니 메인페이지로 이동합니다.');
28 | Router.push('/');
29 | }
30 | }, [me && me.id]);
31 |
32 | const onSubmit = useCallback((e) => {
33 | e.preventDefault();
34 | if (password !== passwordCheck) {
35 | return setPasswordError(true);
36 | }
37 | if (!term) {
38 | return setTermError(true);
39 | }
40 | return dispatch({
41 | type: SIGN_UP_REQUEST,
42 | data: {
43 | userId: id,
44 | password,
45 | nickname: nick,
46 | },
47 | });
48 | }, [id, nick, password, passwordCheck, term]);
49 |
50 | const onChangePasswordCheck = useCallback((e) => {
51 | setPasswordError(e.target.value !== password);
52 | setPasswordCheck(e.target.value);
53 | }, [password]);
54 |
55 | const onChangeTerm = useCallback((e) => {
56 | setTermError(false);
57 | setTerm(e.target.checked);
58 | }, []);
59 |
60 | if (me) {
61 | return null;
62 | }
63 |
64 | return (
65 | <>
66 |
103 | >
104 | );
105 | };
106 |
107 | export default Signup;
108 |
--------------------------------------------------------------------------------
/ts/front/pages/user/[id].tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Avatar, Card } from 'antd';
4 | import { LOAD_USER_POSTS_REQUEST } from '../../reducers/post';
5 | import { LOAD_USER_REQUEST } from '../../reducers/user';
6 | import PostCard from '../../containers/PostCard';
7 |
8 | const User = () => {
9 | const { mainPosts } = useSelector((state) => state.post);
10 | const { userInfo } = useSelector((state) => state.user);
11 |
12 | return (
13 |
14 | {userInfo
15 | ? (
16 |
19 | 짹짹
20 |
21 | {userInfo.Posts}
22 |
,
23 |
24 | 팔로잉
25 |
26 | {userInfo.Followings}
27 |
,
28 |
29 | 팔로워
30 |
31 | {userInfo.Followers}
32 |
,
33 | ]}
34 | >
35 | {userInfo.nickname[0]}}
37 | title={userInfo.nickname}
38 | />
39 |
40 | )
41 | : null}
42 | {mainPosts.map((c) => (
43 |
44 | ))}
45 |
46 | );
47 | };
48 |
49 | User.getInitialProps = async (context) => {
50 | const id = parseInt(context.query.id, 10);
51 | console.log('user getInitialProps', id);
52 | context.store.dispatch({
53 | type: LOAD_USER_REQUEST,
54 | data: id,
55 | });
56 | context.store.dispatch({
57 | type: LOAD_USER_POSTS_REQUEST,
58 | data: id,
59 | });
60 | return { id };
61 | };
62 |
63 | export default User;
64 |
--------------------------------------------------------------------------------
/ts/front/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeroCho/ts-nodebird/af29bc483ea6f0630e426f0dc82f4d1b789aa423/ts/front/public/favicon.ico
--------------------------------------------------------------------------------
/ts/front/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import user, { IUserReducerState } from './user';
3 | import post, { IPostReducerState } from './post';
4 |
5 | export interface IReducerState {
6 | user: IUserReducerState;
7 | post: IPostReducerState;
8 | }
9 |
10 | const rootReducer = combineReducers({
11 | user,
12 | post,
13 | });
14 |
15 | export default rootReducer;
16 |
--------------------------------------------------------------------------------
/ts/front/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { all, fork } from 'redux-saga/effects';
2 | import axios from 'axios';
3 | import user from './user';
4 | import post from './post';
5 | import { backUrl } from '../config/config';
6 |
7 | axios.defaults.baseURL = `${backUrl}/api`;
8 |
9 | export default function* rootSaga() {
10 | yield all([
11 | fork(user),
12 | fork(post),
13 | ]);
14 | }
15 |
--------------------------------------------------------------------------------
/ts/front/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "exclude": [
22 | "node_modules"
23 | ],
24 | "include": [
25 | "next-env.d.ts",
26 | "**/*.ts",
27 | "**/*.tsx"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/ts/front/utils/useInput.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export default (initValue = null) => {
4 | const [value, setter] = useState(initValue);
5 | const handler = useCallback((e) => {
6 | setter(e.target.value);
7 | }, []);
8 | return [value, handler];
9 | };
10 |
--------------------------------------------------------------------------------