reducers/post.js
export const initialState = {
mainPosts: [{···
}],
imagePaths: [], // 이미지 경로 저장
postAdded: false, // 게시글 추가 완료여부
}
// 게시글 작성
const ADD_POST = 'ADD_POST'; // 변수로 지정해 주면 오타 났을 경우 잘 알 수 있음
export const addPost = {
type: ADD_POST,
}
// 가짜 객체
const dummyPost = {
id: 2,
content: '더미데이터입니다',
User: {
id: 1,
nickname: 'onezeun',
},
Images: [],
Comments: [],
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case ADD_POST:
return {
...state,
mainPosts: [dummyPost, ...state.mainPosts],
postAdded: true,
}
default:
return state;
}
}
export default reducer;
pages/index.js
import React from 'react';
import { useSelector } from 'react-redux';
import AppLayout from '../components/AppLayout';
import PostForm from '../components/PostForm';
import PostCard from '../components/PostCard';
const Home = () => {
const { isLoggedIn } = useSelector((state) => state.user);
const { mainPosts } = useSelector((state) => state.post);
return (
<AppLayout>
{isLoggedIn && <PostForm />}
{mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
</AppLayout>
);
};
export default Home;
components/PostForm.js
import React, { useState, useCallback, useRef } from 'react';
import { Button, Form, Input } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { addPost } from '../reducers/post';
const PostForm = () => {
const { imagePaths } = useSelector((state) => state.post);
const dispatch = useDispatch();
const imageInput = useRef();
const [text, setText] = useState('');
const onChangeText = useCallback ((e) => {
setText(e.target.value);
}, []);
const onSubmit = useCallback(() => {
dispatch(addPost);
setText('');
}, []);
const onClickImageUpload = useCallback(() => {
imageInput.current.click();
}, [imageInput.current]);
return (
<Form
style={{ margin: '10px 0 20px' }}
encType="multipart/from-data"
onFinish={onSubmit}
>
<Input.TextArea
value={text}
onChange={onChangeText}
maxLength={140}
placeholder="어떤 신기한 일이 있었나요?"
/>
<div>
<input type="file" multiple hidden ref={imageInput} />
<Button onClick={onClickImageUpload}>이미지 업로드</Button>
<Button type="primary" style={{ float: 'right' }} htmlType="submit">
짹짹
</Button>
</div>
<div>
{imagePaths.map((v) => (
<div key={v} style={{ display: 'inline-block' }}>
<img src={v} style={{ width: '200px' }} alt={v} />
<div>
<Button>제거</Button>
</div>
</div>
))}
</div>
</Form>
);
};
export default PostForm;
components/PostCard.js
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Card, Popover } from 'antd';
import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone } from '@ant-design/icons';
import ButtonGroup from 'antd/lib/button/button-group';
import { useSelector } from 'react-redux';
import Avatar from 'antd/lib/avatar/avatar';
import PostImages from './PostImages';
const PostCard = ({ post }) => {
const [liked, setLiked] = useState(false);
const [commentFormOpened, setCommentFormOpened] = useState(false);
const onToggleLike = useCallback(() => {
// false는 true로 true는 false로 (이전데이터를 기반으로 다음 데이터를 만듦)
setLiked((prev) => !prev);
}, []);
const onToggleComment = useCallback(() => {
setCommentFormOpened((prev) => !prev);
}, []);
const id = useSelector((state) => state.user.me?.id);
return (
<div style={{ marginBottom: 20 }}>
<Card
cover={post.Images[0] && <PostImages images={post.Images} />}
// 배열 안에는 항상 key를 넣어줘야 함
actions={[
<RetweetOutlined key="retweet" />,
liked
? <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onToggleLike} />
: <HeartOutlined key="heart" onClick={onToggleLike} />,
<MessageOutlined key="comment" onClick={onToggleComment} />,
<Popover
key="more"
content={
<ButtonGroup>
{/* 내 ID와 작성자 ID가 같을때 수정, 삭제 가능 다르면 신고 가능 */}
{id && post.User.id === id ? (
<>
<Button>수정</Button>
<Button type="danger">삭제</Button>
</>
) : (
<Button>신고</Button>
)}
</ButtonGroup>
}
>
<EllipsisOutlined />
</Popover>,
]}
>
{/* 게시글 부분 */}
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={post.content}
/>
<Button></Button>
</Card>
{commentFormOpened && (
<div>
댓글부분
</div>
)}
{/*
<CommentForm />
<Comments />
*/}
</div>
);
};
PostCard.propTypes = {
// 더 자세하게 작성하려면 shape을 쓰고 안에 속성을 넣어주면 됨
post: PropTypes.shape({
id: PropTypes.number,
user: PropTypes.object,
content: PropTypes.string,
createdAt: PropTypes.object,
Comments: PropTypes.arrayOf(PropTypes.object),
Images: PropTypes.arrayOf(PropTypes.object),
}).isRequired,
};
export default PostCard;
(optional chaining) 연산자옵셔널이 연달아 호출된 것을 의미
const { me } = useSelector((state) => state.user);
const id = me?.id;
// 옵셔널 체이닝을 안쓰면?
const id = me && me.id
한번에 쓰기
const id = useSelector((state) => state.user.me?.id);
PropTypes.shapecomponents/PostCard.js
...
{commentFormOpened && (
<div>
<CommentForm post={post}/>
<List
header={`${post.Comments.length}개의 댓글`}
itemLayout="horizontal"
dataSource={post.Comments}
renderItem={(item) => (
<li>
<Comment
author={item.User.nickname}
avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
content={item.content}
/>
</li>
)}
/>
</div>
)}
CommentForm에post를 넘겨주는 이유어떤 게시글에 댓글을 달건지 정보가 필요하기 때문
게시글의 id를 받아야 함
components/CommentForm.js
import React, { useCallback } from 'react';
import PropTypes from 'prop-types'
import { Button, Form, Input } from 'antd';
import useInput from '../hooks/useInput';
import { useSelector } from 'react-redux';
const CommentForm = ({ post }) => {
const id = useSelector((state) => state.user.me?.id)
const [commentText,onChangeCommentText] = useInput('');
const onSubmitComment = useCallback(() => {
console.log(post.id, commentText)
}, [commentText]);
return (
<Form onFinish={onSubmitComment}>
<Form.Item style={{ position: 'relative', margin: 0 }}>
<Input.TextArea value={commentText} onChange={onChangeCommentText} rows={4} />
<Button style={{ position: 'absolute', right: 0, bottom: -40 }} type="primary" htmlType='submit'>삐약</Button>
</Form.Item>
</Form>
);
};
CommentForm.propTypes = {
post: PropTypes.object.isRequired,
}
export default CommentForm;