NEXTJS 로 twitter클론 해보기(7. 더미데이터로 post업로드 하기 )

73224 단어 nextjsReactReact

1. 먼저 post reducer을 만들어 볼까요?

post state구성

export const initialState = {
  mainPosts: [
    {
      id: 1, //post id
      User: { //post를 upload한 user
        id: 1, //user id 
        nickname: "leejaehoon", // user nickname
      },
      content: "첫번째 게시글 #해시태그 #익스프레스", //post content
      Image: [ //post 이미지 
        {
          src: "https://gimg.gilbut.co.kr/book/BN001985/rn_view_BN001985.jpg",
        },
        {
          src: "https://gimg.gilbut.co.kr/book/BN001985/rn_view_BN001958.jpg",
        },
        {
          src: "https://gimg.gilbut.co.kr/book/BN001985/rn_view_BN001985.jpg",
        },
      ],
      Comments: [ //post 댓글 
        {
          User: { //댓글단 user 
            nickname: "nero", 
          },
          content: "우와 개정판이 나왔군요~",
        },
        {
          User: {
            nickname: "hero",
          },
          content: "얼른  사고싶어용",
        },
      ],
    },
  ],
  imagePaths: [], //이미지 경로 
  postAdded: false, // 포스트 여부 
};

post Upload시 action만들기

const ADD_POST = "ADD_POST";
export const addPost = {
  type: ADD_POST,
};

post Upload시 Add할 더미데이터 만들기

const dummyPost = {
  id: 2,
  content: "더미데이터입니다.",
  User: {
    id: 1,
    nickname: "zerocho",
  },
  Images: [],
  Comments: [],
};

addPost dispatch시 리턴값 만들기

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_POST:
      return {
        ...state,
        mainPosts: [dummyPost, ...state.mainPosts], //앞쪽에 넣어줘야 젤 앞에 배치된다.
        postAdded: true, 
      };
    default:
      return state;
  }
};

2. pages/index.js 구성하기

isLoggedIn이 true일때 즉 로그인 했을때만 PostForm 컴포넌트를 보여줍니다.

mainPosts의 갯수대로 PostCard 컴포넌트를 보여줍니다.

import AppLayout from "../components/AppLayout";
import { useSelector } from "react-redux";
import PostCard from "../components/PostCard";
import PostForm from "../components/PostForm";
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;

PostForm 컴포넌트

onFinish이벤트가 발생했을때 dispatch(addPost)를 해줍니다.

import { Button, Form, Input } from "antd";
import { useCallback, useState, useRef } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { addPost } from "../reducers/post";

const PostForm = () => {
  const dispatch = useDispatch();
  const imageInput = useRef();
  const [text, setTest] = useState("");

  const onClickImageUpload = useCallback(() => {
    imageInput.current.click();
  }, [imageInput.current]);

  const onChangeTest = e => {
    setTest(e.target.value);
  };
  const onSubmitBtn = useCallback(() => {
    dispatch(addPost);
    setTest("");
  }, []);
  return (
    <Form
      style={{ margin: "10px 0 20px" }}
      encType="multipart/form-data"
      onFinish={onSubmitBtn}
    >
      <Input.TextArea
        value={text}
        onChange={onChangeTest}
        maxLength={140}
        placeholder="어떤 신기한 일이 있었나요?"
      />
      <div>
        <input type="file" multiple hidden ref={imageInput} />
        <Button onClick={onClickImageUpload}>이미지 업로드</Button>
        <Button type="primary" htmlType="submit" style={{ float: "right" }}>
          짹짹
        </Button>
      </div>
      <div>
        {imagePaths.map(v => (
          <div key={v} style={{ display: "lnline-block" }}>
            <img src={v} style={{ width: "200px" }} alt={v} />
            <div>
              <Button>제거</Button>
            </div>
          </div>
        ))}
      </div>
    </Form>
  );
};

export default PostForm;

PostCard 컴포넌트

import { Avatar, Button, Card, Popover } from "antd";
import PropTypes from "prop-types";
import {
  RetweetOutlined,
  HeartOutlined,
  EllipsisOutlined,
  MessageOutlined,
  HeartTwoTone,
} from "@ant-design/icons";
import { useSelector } from "react-redux";
import PostImages from "./PostImages";
import { useCallback, useState } from "react";

const PostCard = ({ post }) => {
  const [liked, setLiked] = useState(false);
  const [commentFormOpened, setCommentFormOpened] = useState(false);

  const id = useSelector(state => state.user.me?.id); //옵셔널 체이닝 문법

  const onToggleLike = useCallback(() => { //liked가 true면 빨간색으로 
    setLiked(prev => !prev);
  }, [liked]);

  const onToggleComment = useCallback(() => { //commentFormOpened이 true면 댓글부분 보이게
    setCommentFormOpened(prev => !prev);
  }, [commentFormOpened]);
  return (
    <div style={{ marginTop: 20 }}>
      <Card
        cover={post.Images[0] && <PostImages images={post.Images} />}
        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={
              <Button.Group>
                {id && post.User.id === id ? (
                  <>
                    <Button>수정</Button>
                    <Button type="danger">삭제</Button>
                  </>
                ) : (
                  <Button>신고</Button>
                )}
              </Button.Group>
            }
          >
            <EllipsisOutlined />
          </Popover>,
        ]}
      >
        <Card.Meta
          avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
          title={post.User.nickname}
          description={post.content}
        />
      </Card>
      {commentFormOpened && <div>댓글 부분</div>}
      {/* <CommentForm /> */}
      {/* <Comments /> */}
    </div>
  );
};

PostCard.propTypes = {
  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; ****

댓글 구현하기

PostCard 컴포넌트의 일부분

commentFormOpened가 true일때 즉 댓글 버튼을 눌렀을때 댓글이 보이게 해줍니다.

		{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 컴포넌트

import { Form, Input, Button } from "antd";
import { useCallback } from "react";
import useInput from "../hooks/useInput";
import PropTypes from "prop-types";
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}
      style={{ position: "relative", margin: 0 }}
    >
      <Form.Item>
        <Input.TextArea
          value={commentText}
          onChange={onChangeCommentText}
          rows={4}
        />
        <Button
          type="primary"
          htmlType="submit"
          style={{ position: "absolute", right: 0, bottom: -40 }}
        >
          댓글 추가
        </Button>
      </Form.Item>
    </Form>
  );
};

CommentForm.propTypes = {
  post: PropTypes.object.isRequired,
};

export default CommentForm;

PostImages 컴포넌트

import PropTypes from "prop-types";
import { useCallback, useState } from "react";
import { Carousel } from "antd";

const PostImages = ({ images }) => {
  const contentStyle = {
    maxWidth: "100%",
    witdth: "100%",
  };
  return (
    <>
      <Carousel style={contentStyle} autoplay>
        {images.map(image => (
          <div>
            <img src={image.src} style={contentStyle} alt={image.src} />
          </div>
        ))}
      </Carousel>
    </>
  );
};

PostImages.propTypes = {
  images: PropTypes.arrayOf(PropTypes.object),
};

export default PostImages;

post reducer state에 있는 maiPosts의 content에는 해시태그가 들어가는데 각 해시태그마다 링크를 걸어주려고 합니다.

postData에는 post의 content가 들어가 있습니다.

정규표현식을 통해 #이 들어간 단어들을 분리하여 배열로 만든후 #으로 시작하는 단어들만 Link를 해뒀습니다.

PostCardContent.js

import PropTypes from "prop-types";
import Link from "next/link";

const PostCardContent = ({ postData }) => {
  return (
    <div>
      {postData.split(/(#[^\s#]+)/g).map((str, i) => {
        if (str.startsWith("#")) {
          return (
            <Link key={i} href={`/hashtag/${str.slice(1)}`}>
              <a>{str}</a>
            </Link>
          );
        } else {
          return str;
        }
      })}
    </div>
  );
};

PostCardContent.propTypes = {
  postData: PropTypes.string.isRequired,
};

export default PostCardContent;

좋은 웹페이지 즐겨찾기