본문 바로가기

카테고리 없음

TeamProject_240601 (뉴스피드 프로젝트)

📦src
 ┣ 📂api
 ┃ ┗ 📜supabaseClient.js
 ┣ 📂assets
 ┃ ┣ 📜back.png
 ┃ ┣ 📜ddabong.png
 ┃ ┣ 📜Dragonlogo3.png
 ┃ ┣ 📜github.png
 ┃ ┣ 📜loginbtn.png
 ┃ ┣ 📜nabc.png
 ┃ ┣ 📜notion.png
 ┃ ┣ 📜password.png
 ┃ ┗ 📜seach.png
 ┣ 📂components
 ┃ ┣ 📜Footer.jsx
 ┃ ┗ 📜Header.jsx
 ┣ 📂layouts
 ┃ ┣ 📜GlobalStyle.jsx
 ┃ ┗ 📜Layout.jsx
 ┣ 📂pages
 ┃ ┣ 📂AuthPage
 ┃ ┃ ┗ 📜AuthPage.jsx
 ┃ ┣ 📂DetailPage
 ┃ ┃ ┣ 📜DetailPage.jsx
 ┃ ┃ ┗ 📜DetailPage.styles.js
 ┃ ┣ 📂EditPostPage
 ┃ ┃ ┣ 📜EditPostPage.jsx
 ┃ ┃ ┗ 📜EditPostPage.styles.js
 ┃ ┣ 📂HomePage
 ┃ ┃ ┣ 📜HomePage.jsx
 ┃ ┃ ┗ 📜HomePage.styles.js
 ┃ ┣ 📂Mypage
 ┃ ┃ ┗ 📜Mypage.jsx
 ┃ ┗ 📂WritePostPage
 ┃ ┃ ┣ 📜WritePostPage.jsx
 ┃ ┃ ┗ 📜WritePostPage.styles.js
 ┣ 📂redux
 ┃ ┣ 📂slices
 ┃ ┃ ┗ 📜postSlice.js
 ┃ ┗ 📂store
 ┃ ┃ ┗ 📜postStore.js
 ┣ 📂routes
 ┃ ┗ 📜router.jsx
 ┣ 📂styledComponents
 ┃ ┣ 📜Footer.jsx
 ┃ ┗ 📜Header.jsx
 ┣ 📜App.jsx
 ┗ 📜main.jsx

 

 

import { configureStore } from '@reduxjs/toolkit';
import postsReducer from '../slices/postSlice';

const store = configureStore({
  reducer: {
    posts: postsReducer
  }
});

export default store;

 

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import supabase from '../../api/supabaseClient';

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const { data } = await supabase.from('posts').select();
  return data;
});

export const createPost = createAsyncThunk('posts/createPost', async ({ title, content }) => {
  const { data } = await supabase.from('posts').insert({ title, content }).select();
  return data[0];
});

export const deletePost = createAsyncThunk('posts/deletePost', async (id) => {
  const { data } = await supabase.from('posts').delete().eq('id', id).select();
  return data[0];
});

export const updatePost = createAsyncThunk('posts/updatePost', async ({ id, title, content }) => {
  const { data } = await supabase.from('posts').update({ title, content }).eq('id', id).select();
  return data[0];
});

const postsSlice = createSlice({
  name: 'posts',
  initialState: {
    posts: [],
    status: 'idle',
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.posts = action.payload;
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      })
      .addCase(createPost.fulfilled, (state, action) => {
        state.posts.push(action.payload);
      })
      .addCase(deletePost.fulfilled, (state, action) => {
        state.posts = state.posts.filter((post) => post.id !== action.payload.id);
      })
      .addCase(updatePost.fulfilled, (state, action) => {
        const index = state.posts.findIndex((post) => post.id === action.payload.id);
        if (index !== -1) {
          state.posts[index] = action.payload;
        }
      });
  }
});

export default postsSlice.reducer;

 

import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { createPost } from '../../redux/slices/postSlice';
import {
  Container,
  Header,
  Title,
  Subtitle,
  Form,
  Label,
  Input,
  Textarea,
  ButtonContainer,
  Button
} from './WritePostPage.styles';

const WritePostPage = () => {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const handleCreatePost = () => {
    if (title && content) {
      dispatch(createPost({ title, content }));
      navigate('/');
    } else {
      alert('제목과 내용을 입력해주세요.');
    }
  };

  return (
    <Container>
      <Header>
        <Title>게시글 작성</Title>
        <Subtitle>게시글을 작성하고 업로드하세요</Subtitle>
      </Header>
      <Form>
        <Label>게시글 제목</Label>
        <Input type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
        <Label>게시글 내용</Label>
        <Textarea value={content} onChange={(e) => setContent(e.target.value)} />
        <ButtonContainer>
          <Button onClick={handleCreatePost}>업로드</Button>
          <Button onClick={() => navigate('/')}>뒤로가기</Button>
        </ButtonContainer>
      </Form>
    </Container>
  );
};

export default WritePostPage;

 

import styled from 'styled-components';

export const Container = styled.div`
  width: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background-color: #f9f9f9;
`;

export const Header = styled.header`
  margin-bottom: 20px;
  text-align: center;
`;

export const Title = styled.h1`
  font-size: 24px;
  color: #333;
`;

export const Subtitle = styled.p`
  font-size: 16px;
  color: #666;
`;

export const Form = styled.form`
  display: flex;
  flex-direction: column;
  width: 100%;
`;

export const Label = styled.label`
  margin-bottom: 10px;
  font-size: 14px;
  color: #333;
`;

export const Input = styled.input`
  padding: 10px;
  margin-bottom: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
`;

export const Textarea = styled.textarea`
  padding: 10px;
  height: 200px;
  margin-bottom: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
  resize: none;
`;

export const ButtonContainer = styled.div`
  display: flex;
  justify-content: space-between;
`;

export const Button = styled.button`
  padding: 10px 20px;
  font-size: 16px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  background-color: #007bff;
  color: white;
  &:hover {
    background-color: #0056b3;
  }
`;

 

import { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from '../../redux/slices/postSlice';
import { Link } from 'react-router-dom';
import { PostList, PostItem, PostTitle, PostContent, Button, ProfileImage, Section } from './HomePage.styles';

import supabase from '../../api/supabaseClient';

function HomePage() {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts.posts);
  const status = useSelector((state) => state.posts.status);
  const error = useSelector((state) => state.posts.error);

  const [signIn, setSignIn] = useState(false);
  const [profileUrl, setProfileUrl] = useState('');
  const fileInputRef = useRef(null);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchPosts());
    }
  }, [status, dispatch]);

  async function signInWithGithub() {
    await supabase.auth.signInWithOAuth({
      provider: 'github'
    });
  }

  async function checkSignIn() {
    const session = await supabase.auth.getSession();
    const isSignIn = !!session.data.session;

    setSignIn(isSignIn);
  }

  async function signOut() {
    await supabase.auth.signOut();
    checkSignIn();
  }

  function checkProfile() {
    const { data } = supabase.storage.from('avatars').getPublicUrl('avatar_1717215361574.png');
    setProfileUrl(data.publicUrl);
  }

  async function handleFileInputChange(files) {
    const [file] = files;

    if (!file) {
      return;
    }

    const { data } = await supabase.storage.from('avatars').upload(`avatar_${Date.now()}.png`, file);

  }

  useEffect(() => {
    checkSignIn();
    checkProfile();
  }, []);

  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'failed') {
    return <div>Error: {error}</div>;
  }

  return (
    <main>
      <PostList>
        {posts.map((post) => (
          <PostItem key={post.id}>
            <PostTitle>{post.title}</PostTitle>
            <br />
            <PostContent>{post.content}</PostContent>
            <br />
            <Link to={`/edit/${post.id}`}>
              <Button type="button">게시글 수정</Button>
            </Link>
          </PostItem>
        ))}
      </PostList>

      <Section>
        {signIn ? (
          <>
            <input
              onChange={(e) => handleFileInputChange(e.target.files)}
              type="file"
              ref={fileInputRef}
              className="hidden"
            />
            <ProfileImage src={profileUrl} alt="profile" onClick={() => fileInputRef.current.click()} />
            <Button onClick={signOut}>로그아웃</Button>
          </>
        ) : (
          <Button onClick={signInWithGithub}>로그인</Button>
        )}
        <Link to={`/write`}>
          <Button type="button">게시글 작성</Button>
        </Link>
      </Section>
    </main>
  );
}

export default HomePage;

 

import styled from 'styled-components';

export const PostList = styled.ul`
  list-style-type: none;
  padding: 0;
  width: 1320px;
  margin: 0 auto;
  display: flex;
  justify-content: start;
  gap: 20px;
  flex-wrap: wrap;
`;

export const PostItem = styled.li`
  border-bottom: 1px solid #ccc;
  margin-bottom: 10px;
  padding-bottom: 10px;
`;

export const PostTitle = styled.h2`
  font-size: 24px;
  font-weight: bold;
  margin: 0;
`;

export const PostContent = styled.p`
  white-space: pre-wrap;
  font-size: 16px;
  margin: 10px 0 0 0;
`;

export const Button = styled.button`
  margin-top: 10px;
  padding: 8px 12px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #0056b3;
  }
`;

export const ProfileImage = styled.img`
  border-radius: 50%;
  cursor: pointer;
  width: 45px;
  height: 45px;
`;

export const Section = styled.section`
  display: flex;
  gap: 10px;
  align-items: center;
`;

 

import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams, useNavigate } from 'react-router-dom';
import { fetchPosts, deletePost, updatePost } from '../../redux/slices/postSlice';
import {
  Container,
  Header,
  Title,
  Subtitle,
  Form,
  Label,
  Input,
  Textarea,
  ButtonContainer,
  Button
} from './EditPostPage.styles';

const EditPostPage = () => {
  const { id } = useParams();
  const postId = parseInt(id, 10);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const post = useSelector((state) => state.posts.posts.find((post) => post.id === postId));
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  useEffect(() => {
    if (!post) {
      dispatch(fetchPosts());
    } else {
      setTitle(post.title);
      setContent(post.content);
    }
  }, [dispatch, post]);

  const handleUpdatePost = () => {
    if (title && content) {
      dispatch(updatePost({ id: postId, title, content }));
      navigate('/');
    } else {
      alert('제목과 내용을 입력해주세요.');
    }
  };

  const handleDeletePost = () => {
    const confirmDelete = window.confirm('정말 이 글을 삭제하시겠습니까?');
    if (confirmDelete) {
      dispatch(deletePost(postId));
      navigate('/');
    }
  };

  return (
    <Container>
      <Header>
        <Title>게시글 수정</Title>
        <Subtitle>게시글을 수정하고 업로드하세요</Subtitle>
      </Header>
      <Form>
        <Label>게시글 제목</Label>
        <Input type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
        <Label>게시글 내용</Label>
        <Textarea value={content} onChange={(e) => setContent(e.target.value)} />
        <ButtonContainer>
          <Button onClick={handleUpdatePost}>수정</Button>
          <Button onClick={handleDeletePost}>삭제</Button>
          <Button onClick={() => navigate('/')}>돌아가기</Button>
        </ButtonContainer>
      </Form>
    </Container>
  );
};

export default EditPostPage;

 

import styled from 'styled-components';

export const Container = styled.div`
  width: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background-color: #f9f9f9;
`;

export const Header = styled.header`
  margin-bottom: 20px;
  text-align: center;
`;

export const Title = styled.h1`
  font-size: 24px;
  color: #333;
`;

export const Subtitle = styled.p`
  font-size: 16px;
  color: #666;
`;

export const Form = styled.form`
  display: flex;
  flex-direction: column;
  width: 100%;
`;

export const Label = styled.label`
  margin-bottom: 10px;
  font-size: 14px;
  color: #333;
`;

export const Input = styled.input`
  padding: 10px;
  margin-bottom: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
`;

export const Textarea = styled.textarea`
  padding: 10px;
  height: 200px;
  margin-bottom: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
  resize: none;
`;

export const ButtonContainer = styled.div`
  display: flex;
  justify-content: space-between;
`;

export const Button = styled.button`
  padding: 10px 20px;
  font-size: 16px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  background-color: #007bff;
  color: white;
  &:hover {
    background-color: #0056b3;
  }
`;