[React : mini-project] 게시글 공개/비공개 설정하기

문지은·2023년 8월 1일
0

React

목록 보기
20/24
post-thumbnail

공개 여부 체크

  • 부트스트랩 Check 컴포넌트를 사용하여 공개 여부를 체크할 수 있는 form 작성
<div className='form-check mb-3'>
    <input className='form-check-input'
            type='checkbox'
            checked={publish}
            onChange={onChangePublish} />
    <label className='form-check-label'>
      Publish
    </label>
</div>
  • publish state 값이 true/false 일 때 check 표시/비표시
    • publish 값이 변경되면 state를 업데이트하는 onChangePublish 함수 작성
const onChangePublish = (event) => {
  setPublish(event.target.checked)
}
  • Form 을 제출할 때 publish 값도 함께 DB에 저장하도록 onSubmit 함수 수정
const onSubmit = () => {
  if (editing) {
    axios.patch(`http://localhost:3001/posts/${id}`, {
      title: title,
      body: body,
      publish: publish
    }) .then(() => {
      navigate(`/blogs/${id}`)
    })
  } else {
    axios.post('http://localhost:3001/posts', {
      title: title,
      body: body,
      publish: publish,
      createdAt: Date.now()
    }).then(() => {
      navigate('/blogs')
    })
  }
}
  • publish 값만 변경해도 edit 제출할 수 있도록 originalPublish state 만들고 isEdited 함수 수정
    • get 요청시 originalPublish 값 업데이트
const {id} = useParams();
useEffect(() => {
  if (editing) {
    axios.get(`http://localhost:3001/posts/${id}`)
      .then((response) => {
      // console.log(response.data)
      setTitle(response.data.title)
      setOriginalTitle(response.data.title)
      setBody(response.data.body)
      setOriginalBody(response.data.body)
      setPublish(response.data.publish)
      setOriginalPublish(response.data.publish)
    })
  }
}, [id, editing])

const isEdited = () => {
  return title !== originalTitle || body !== originalBody || publish !== originalPublish
}
  • 최종 수정 BlogForm

src/componenets/BlogForm.jsx

import React from 'react'
import { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import { bool } from 'prop-types';

// Edit Page
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';

function BlogForm({editing}) {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const navigate = useNavigate();

  const [originalTitle, setOriginalTitle] = useState('');
  const [originalBody, setOriginalBody] = useState('');
  const [originalPublish, setOriginalPublish] = useState('');

  // 공개 여부 결정
  const [publish, setPublish] = useState(false);

  // Edit Page
  const {id} = useParams();
  useEffect(() => {
    if (editing) {
      axios.get(`http://localhost:3001/posts/${id}`)
      .then((response) => {
        // console.log(response.data)
        setTitle(response.data.title)
        setOriginalTitle(response.data.title)
        setBody(response.data.body)
        setOriginalBody(response.data.body)
        setPublish(response.data.publish)
        setOriginalPublish(response.data.publish)
      })
    }
  }, [id, editing])

  const isEdited = () => {
    return title !== originalTitle || body !== originalBody || publish !== originalPublish
  }

  const onSubmit = () => {
      if (editing) {
        axios.patch(`http://localhost:3001/posts/${id}`, {
          title: title,
          body: body,
          publish: publish
        }) .then(() => {
          navigate(`/blogs/${id}`)
        })
      } else {
        axios.post('http://localhost:3001/posts', {
        title: title,
        body: body,
        publish: publish,
        createdAt: Date.now()
        }).then(() => {
          navigate('/blogs')
        })
      }
  }

  const goBack = () => {
    if (editing) {
    navigate(`/blogs/${id}`)
    } else {
      navigate('/blogs/')
    }
  }

  const onChangePublish = (event) => {
    setPublish(event.target.checked)
  }

  return (
    <div>
        <h1>{editing ? 'Edit' : 'Create'} a blog post</h1>
        <div className='mb-3'>
            <label className='form-label'>Title</label>
            <input 
            className='form-control'
            value={title}
            onChange={(event) => {
                setTitle(event.target.value)
            }}
            />
        </div>
        <div className='mb-3'>
            <label className='form-label'>Body</label>
            <textarea 
            className='form-control'
            value={body}
            onChange={(event) => {
                setBody(event.target.value)
            }}
            rows={10}
            />
        </div>

        {/* 공개 여부 결정하기 */}
        <div className='form-check mb-3'>
            <input className='form-check-input'
                    type='checkbox'
                    checked={publish}
                    onChange={onChangePublish} />
            <label className='form-check-label'>
              Publish
            </label>
        </div>


        <button 
            className='btn btn-primary'
            onClick={onSubmit}
            disabled={editing && !isEdited()}
            >{editing ? 'Edit' : 'Post'}</button>
        <button 
          className='btn btn-danger ms-3'
          onClick={goBack}
          >Cancel</button>
    </div>
  )
}

BlogForm.propTypes = {
  editing: bool
}

BlogForm.defaultProps = {
  editing: false
}

export default BlogForm
  • BlogForm에 Publish 체크박스가 생기고, 체크하고 폼 제출시 DB에 잘 저장되는 것 확인

비공개 게시글 리스트 페이지에서 제거

  • 리스트 페이지의 renderBlogList 함수를 filter 함수를 사용하여 publish가 true 값인 post만 출력하도록 수정
const renderBlogList = () => {
    if (loading) {
      return (
        <LoadingSpinner/>
      )
    }

    if (posts.length === 0) {
      return (<div>'No blog posts found'</div>)
    }
	
  	// publish가 true 인 post만 filter
    return posts.filter(post=> {
        return post.publish
      }).map(post => {
      return (
        <Card 
          key={post.id} 
          title={post.title} 
          onClick={() => navigate(`/blogs/${post.id}`)}>
          <button 
            className="btn btn-danger btn-sm" 
            onClick={(event) => deleteBlog(event, post.id)}>
            Delete</button>
        </Card>
      )
    })
  }

전체 글 조회 페이지 만들기

비공개 글을 포함한 전체 글을 조회할 수 있는 Admin 페이지를 만들어보자.

  • 라우트 추가

routes.js

...
import AdminPage from './pages/AdminPage';


const routes = [
...
  {
    path:'/admin',
    element: <AdminPage />
  },
...
]

export default routes;
  • 기존에 ListPage.jsxrenderBlogList 라는 함수를 통해 게시글 리스트를 출력했었는데, 이 부분을 따로 빼서 BlogList.jsx라는 함수 컴포넌트를 만든다.
    • 해당 컴포넌트에 isAdmin 이라는 boolean 값을 props로 갖게 하고, 기본 값은 false로 정한다.
    • isAdmin 값이 true 일 경우에만 delete 버튼이 출력되도록 수정
    • isAdmin 이 true일 경우에는 모든 post를, false 일 경우에는 publish 가 true 인 post를 반환하도록 함수 수정

src/components/BlogList.jsx

import React from 'react'
import axios from 'axios'
import { useState, useEffect } from 'react'

import Card from '../components/Card';
import LoadingSpinner from '../components/LoadingSpinner';

import { Link, useNavigate } from 'react-router-dom';
import { bool } from 'prop-types';

function BlogList({isAdmin}) {
  const [posts, setPosts] = useState([]);
  const navigate = useNavigate();

  const [loading, setLoading] = useState(true);

  const getPosts = () => {
    axios.get('http://localhost:3001/posts')
    .then((response) => {
      setPosts(response.data);
      // post 불러온 이후에 로딩중 상태 false로 변경
      setLoading(false);
    })
  }

  const deleteBlog = (event, id) => {
    // 이벤트 버블링 막기
    event.stopPropagation();
    axios.delete(`http://localhost:3001/posts/${id}`)

    // 삭제하려는 id와 같지 않은 post만 담아 posts 업데이트
    .then(() => {
      setPosts(prevPosts => {
        return prevPosts.filter(post => {
          return post.id !== id;
        })
      })
    })
  }

  useEffect(() => {
    getPosts();
  }, [])


  if (loading) {
      return (
        <LoadingSpinner/>
      )
    }

    if (posts.length === 0) {
      return (<div>'No blog posts found'</div>)
    }
	// ************** filter 조건 수정
    return posts.filter(post=> {
        return isAdmin || post.publish
      }).map(post => {
      return (
        <Card 
          key={post.id} 
          title={post.title} 
          onClick={() => navigate(`/blogs/${post.id}`)}>
		  // ************** isAdmin 일 경우에만 delete 버튼 출력
          {isAdmin ? <button 
            className="btn btn-danger btn-sm" 
            onClick={(event) => deleteBlog(event, post.id)}>
            Delete</button> : null}

        </Card>
      )
    })
}

// ************** isAdmin props 타입 지정
BlogList.propTypes = {
  isAdmin: bool
}

BlogList.defaultProps = {
  isAdmin: false
}

export default BlogList
  • AdminPageBlogList에 props로 true를, ListPageBlogList에 props로 false를 전달하도록 수정
  • Create 버튼은 AdminPage에만 있도록 수정

src/pages/AdminPage.jsx

import React from 'react'
import BlogList from '../components/BlogList.jsx';
import { Link } from 'react-router-dom';

function AdminPage() {
  return (
    <div>
      <div className='d-flex justify-content-between'>
        <h1>Admin</h1>
        <div>
          <Link to="/blogs/create" className='btn btn-success'>Creat New</Link>
        </div>
      </div>
      <BlogList isAdmin={true}/>
    </div>
  )
}

export default AdminPage

src/pages/ListPage.jsx

import React from 'react'
import BlogList from '../components/BlogList.jsx';

function ListPage() {
  
  return (
    <div>
      <div className='d-flex justify-content-between'>
        <h1>Blogs</h1>
      </div>
      <BlogList/>
    </div>
  )
}

export default ListPage

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글