글쓰는쿼카의 PM 여정

[팀프로젝트] 게시글 CRUD 구현 (2024. 6. 6.) 본문

개발/React

[팀프로젝트] 게시글 CRUD 구현 (2024. 6. 6.)

글쓰는쿼카 joymet33 2024. 6. 10. 16:54

#스파르타코딩클럽 #내일배움캠프(프론트엔드_React) 
학습주제 : React (숙련주차 팀프로젝트 - 뉴스피드)

학습내용 : 게시글 CRUD 구현

학습일 : 2024. 6. 6.

 

<목차>

1. 트러블 슈팅
2. 게시글 작성(Create) 코드
3. 게시글 삭제(Delete) 코드
4. 게시글 수정(Update) 코드

1. 트러블 슈팅 (추가 보충 요함..!)

- 삭제가 왜 안 되지?

 

- 버튼 이슈: 상세페이지에서 게시글 수정일 경우 버튼1 : "수정 완료"/ 버튼2 : "수정 취소", 게시글 삭제일 경우 버튼1 : "삭제 완료", 버튼 2 : "삭제 취소" 

 

- 삼항연산자를 간단하게 만들고 싶은데 어떻게 해야 되지?

 

2. 게시글 작성(Create) 코드

▶ 핵심 코드(요약)

  const handleCreatePost = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.from('POSTS').insert({ title, contents: content }).select().throwOnError();
    if (error) {
      openModal('게시글 실패', '게시글 생성에 실패했습니다.');
      navigate(`/${userId}/blog/posts`);
    } else {
      setPost(...data);
      openModal('게시글 성공', '게시글이 생성되었습니다.');
      navigate(`/${userId}/blog/posts`);
    }
  };

  return (
    <PostWrapper>
      <PostTitle placeholder="게시글의 제목을 입력해주세요." value={title} onChange={(e) => setTitle(e.target.value)} />
      <PostContent
        placeholder="게시글의 내용을 입력해주세요."
        value={content}
        onChange={(e) => setContent(e.target.value)}
      />
      <ButtonWrapper>
        <PostSaveButton onClick={handleCreatePost}>저장</PostSaveButton>
        <PostCancelButton onClick={() => navigate(`/${userId}/blog/posts`)}>취소</PostCancelButton>
      </ButtonWrapper>
    </PostWrapper>
  );
};

 

▶ 전체 코드 (접은글)

 

더보기
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import supabase from '../../config/supabase.js';
import { useModal } from '../../contexts/popup.context.jsx';
const PostCreatingPage = () => {
  const [post, setPost] = useState({
    id: '',
    title: '',
    contents: '',
  });
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const { userId } = useParams();
  const navigate = useNavigate();
  const { openModal } = useModal();

  const handleCreatePost = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.from('POSTS').insert({ title, contents: content }).select().throwOnError();
    if (error) {
      openModal('게시글 실패', '게시글 생성에 실패했습니다.');
      navigate(`/${userId}/blog/posts`);
    } else {
      setPost(...data);
      openModal('게시글 성공', '게시글이 생성되었습니다.');
      navigate(`/${userId}/blog/posts`);
    }
  };

  return (
    <PostWrapper>
      <PostTitle placeholder="게시글의 제목을 입력해주세요." value={title} onChange={(e) => setTitle(e.target.value)} />
      <PostContent
        placeholder="게시글의 내용을 입력해주세요."
        value={content}
        onChange={(e) => setContent(e.target.value)}
      />
      <ButtonWrapper>
        <PostSaveButton onClick={handleCreatePost}>저장</PostSaveButton>
        <PostCancelButton onClick={() => navigate(`/${userId}/blog/posts`)}>취소</PostCancelButton>
      </ButtonWrapper>
    </PostWrapper>
  );
};

export default PostCreatingPage;

const PostWrapper = styled.div`
  width: 100%;
  height: auto;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 50px;
  gap: 30px;
`;

const PostTitle = styled.input`
  width: 90%;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 30px;
  line-height: 50px;
  text-align: center;
  color: #000000;
  border: 1px solid #d2dade;
  border-radius: 10px;
`;

const PostContent = styled.textarea`
  width: 90%;
  min-height: 200px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-size: 20px;
  line-height: 50px;
  text-align: center;
  color: #000000;
  border: 1px solid #d2dade;
  border-radius: 10px;
  background-color: #ffffff;
`;

const ButtonWrapper = styled.div`
  width: 90%;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 20px;
`;
const PostSaveButton = styled.button`
  width: 200px;
  height: 35px;
  padding: 0 10px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 20px;
  background-color: #ff6077;
  border: 1px solid #ff6077;
  border-radius: 10px;
  color: white;
  box-sizing: border-box;
  transition-duration: 250ms;

  &:hover {
    cursor: pointer;
    transform: scale(1.03);
    transition: all 0.1s ease;
    box-shadow: 0px 10px 10px 0px rgba(0, 0, 0, 0.1);
  }
`;
const PostCancelButton = styled.button`
  width: 200px;
  height: 35px;
  padding: 0 10px;
  min-width: 100px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 20px;
  background-color: white;
  border: 1px solid #3aa6b9;
  border-radius: 10px;
  color: #3aa6b9;
  box-sizing: border-box;
  transition-duration: 250ms;

  &:hover {
    cursor: pointer;
    transform: scale(1.03);
    transition: all 0.1s ease;
    box-shadow: 0px 10px 10px 0px rgba(0, 0, 0, 0.1);
  }
`;

 

3. 게시글 삭제(Delete) 코드

▶ 핵심 코드(요약)

const handleDeletePost = async () => {
    if (window.confirm('정말로 게시글을 삭제하시겠습니까?')) {
      const { error } = await supabase.from('POSTS').delete().eq('id', postId);
      if (error) {
        alert('게시글 삭제에 실패했습니다.');
        navigate(`/${userId}/blog/posts`);
      }
      alert('게시글이 삭제되었습니다.');
      navigate(`/${userId}/blog/posts`);
    }
  };

 

▶ 전체 코드 (아래 4번 게시글 수정 코드의 전체 코드(접은글) 참고)

 

4. 게시글 수정(Update) 코드

▶ 핵심 코드(요약)

  const handleTogglePost = async () => {
    const { data, error } = await supabase
      .from('POSTS')
      .update({
        title: title,
        contents: content,
      })
      .eq('id', postId)
      .select();
    if (error) {
      alert('게시글 업데이트에 실패했습니다.');
      navigate(`/${userId}/blog/posts`);
    }

    setPost(...data);
    setIsEditing(false);
    alert('게시글의 수정이 완료되었습니다.');
    navigate(`/${userId}/blog/posts`);
  };

 

▶ 전체 코드 (접은글)

더보기
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import supabase from '../../config/supabase.js';
import { useUser } from '../../contexts/login.context.jsx';
import formatDate, { DATE_FORMATS } from '../../utils/dateFormatUtils.js';

function PostDetailPage() {
  const { userData } = useUser();
  const { postId, userId } = useParams();

  const [post, setPost] = useState({
    id: postId,
    title: '',
    contents: '',
  });
  const [isEditing, setIsEditing] = useState(false);
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const navigate = useNavigate();

  useEffect(() => {
    supabase
      .from('POSTS')
      .select('*')
      .eq('id', postId)
      .then((response) => {
        const { data, error } = response;
        if (error) {
          alert('오류가 발생했습니다.');
          navigate(`/${userId}/blog/posts`);
        }

        const dbPost = data.find((dbData) => dbData.id === postId);
        setPost({
          ...dbPost,
          created_at: formatDate(new Date(dbPost.created_at), DATE_FORMATS.KOREAN),
        });
      });
  }, [postId]);

  const handleTogglePost = async () => {
    const { data, error } = await supabase
      .from('POSTS')
      .update({
        title: title,
        contents: content,
      })
      .eq('id', postId)
      .select();
    if (error) {
      alert('게시글 업데이트에 실패했습니다.');
      navigate(`/${userId}/blog/posts`);
    }

    setPost(...data);
    setIsEditing(false);
    alert('게시글의 수정이 완료되었습니다.');
    navigate(`/${userId}/blog/posts`);
  };

  const handleDeletePost = async () => {
    if (window.confirm('정말로 게시글을 삭제하시겠습니까?')) {
      const { error } = await supabase.from('POSTS').delete().eq('id', postId);
      if (error) {
        alert('게시글 삭제에 실패했습니다.');
        navigate(`/${userId}/blog/posts`);
      }
      alert('게시글이 삭제되었습니다.');
      navigate(`/${userId}/blog/posts`);
    }
  };

  return (
    <>
      <PostWrapper>
        <PostHeaderContainer>
          {isEditing ? (
            <PostTitle placeholder={post.title} onChange={(e) => setTitle(e.target.value)} />
          ) : (
            <PostTitleP>{post.title}</PostTitleP>
          )}
          <PostCreatedAt> {post.created_at}</PostCreatedAt>
          <PostTitleLine />
        </PostHeaderContainer>
        {isEditing ? (
          <PostContent placeholder={post.contents} onChange={(e) => setContent(e.target.value)} />
        ) : (
          <PostContentP>{post.contents}</PostContentP>
        )}
        <ButtonWrapper>
          {userData.userId !== userId ? (
            ''
          ) : (
            <PostSaveButton
              onClick={() => {
                isEditing ? handleTogglePost() : setIsEditing(true);
              }}
            >
              {isEditing ? '수정 완료' : '수정'}
            </PostSaveButton>
          )}
          {userData.userId !== userId ? (
            ''
          ) : isEditing ? (
            <PostCancelButton onClick={() => navigate(`/${userId}/blog/posts`)}>수정 취소</PostCancelButton>
          ) : (
            <PostCancelButton onClick={handleDeletePost}>삭제</PostCancelButton>
          )}
        </ButtonWrapper>
      </PostWrapper>
    </>
  );
}

export default PostDetailPage;

const PostWrapper = styled.div`
  width: 100%;
  height: auto;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 50px;
  gap: 30px;
`;
const PostHeaderContainer = styled.div`
  width: 100%;
  min-height: 100px;
`;

const PostTitle = styled.input`
  width: 90%;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 30px;
  line-height: 50px;
  text-align: center;
  color: #000000;
  border: 1px solid #d2dade;
  border-radius: 10px;
  margin: 0 auto;
`;
const PostTitleP = styled.p`
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 30px;
  line-height: 50px;
  text-align: center;
  color: #000000;
`;

const PostTitleLine = styled.div`
  width: 50px;
  margin: 0 auto;
  border: 1px solid #ff6077;
`;

const PostCreatedAt = styled.div`
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100;
  font-size: 15px;
  line-height: 50px;
  text-align: center;
`;

const PostContent = styled.textarea`
  width: 90%;
  min-height: 200px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-size: 20px;
  line-height: 50px;
  text-align: center;
  color: #000000;
  border: 1px solid #d2dade;
  border-radius: 10px;
  background-color: #ffffff;
`;

const PostContentP = styled.p`
  width: 90%;
  min-height: 200px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-size: 20px;
  line-height: 50px;
  text-align: center;
  color: #000000;
  background-color: #ffffff;
`;

const ButtonWrapper = styled.div`
  width: 90%;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 20px;
`;

const PostSaveButton = styled.button`
  min-width: 160px;
  height: 35px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 20px;
  background-color: #ff6077;
  border: 1px solid #ff6077;
  border-radius: 10px;
  color: white;

  &:hover {
    cursor: pointer;
    transform: scale(1.03);
    transition: all 0.1s ease;
    box-shadow: 0px 10px 10px 0px rgba(0, 0, 0, 0.1);
  }
`;
const PostCancelButton = styled.button`
  min-width: 160px;
  height: 35px;
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-size: 20px;
  background-color: white;
  border: 1px solid #ff6077;
  border-radius: 10px;
  color: #ff6077;

  &:hover {
    cursor: pointer;
    transform: scale(1.03);
    transition: all 0.1s ease;
    box-shadow: 0px 10px 10px 0px rgba(0, 0, 0, 0.1);
  }
`;