juni
TeamProject_뉴스피드 프로젝트 본문
📦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);
setProfileUrl(`https://yzkoayeawivyvwgpnzvu.supabase.co/storage/v1/object/public/avatars/${data.path}`);
}
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;
}
`;
'프로젝트 > 미니 프로젝트' 카테고리의 다른 글
TeamProject_뉴스피드 프로젝트 (0) | 2024.06.04 |
---|---|
TeamProject_뉴스피드 프로젝트 (0) | 2024.06.03 |
SingleProject_리액트 가계부 RTK 개선 (0) | 2024.05.30 |
SingleProject_리액트 가계부 redux (0) | 2024.05.28 |
SingleProject_리액트 가계부 context (0) | 2024.05.26 |