React.js/SNS Project
Day 4
Jinny96
2022. 8. 23. 19:49
1. Flow Chart
2. 폴더 구조
2. 홈 네비게이션 바(상단)
-/Home/Header.jsx
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Grid, Typography } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { getDocs, query, collection, where } from "firebase/firestore";
import {db} from "../../Env/Firebase";
const Header = () => {
const navigate = useNavigate();
const id = sessionStorage.getItem("user_id");
const [userImage, setUserImage] = useState("");
/** 로그인할 때 유저의 id를 세션스토리지에 저장했다.
* 이를 이용해 쿼리문을 만들어 users 테이블에서 id 필드값이 일치하는 것을 찾는다.
* 필드값이 일치하는 문서를 가져와 유저의 대표이미지를 불러온다.
*/
useEffect(() => {
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
setUserImage(doc.data().image);
});
});
}, []);
/** 왼쪽 사진 클릭시 내 피드로 넘어가기 */
const onClickMyFeed = () => {
navigate("/myFeed");
}
/** 오른쪽 플러스 아이콘 클릭시 피드 올리는 페이지로 이동하기 */
const onClickCreateFeed = () => {
navigate("/createFeed");
}
return (
<Grid container style={{ alignItems: "center", textAlign: "center", width: "80%", margin: "auto", marginTop: 70, top: 0, position: "sticky" }}>
<Grid item xs={4}>
{userImage &&
<IconButton onClick={onClickMyFeed} style={{padding: 0}}>
<img src={userImage} style={{ width: 40, height: 40, borderRadius: "50%" }}/>
</IconButton>
}
</Grid>
<Grid item xs={4}>
<Typography variant="h4">
<Link to="/home" style={{ textDecoration: 'none', color: 'black' }}>
Youngstagram
</Link>
</Typography>
</Grid>
<Grid item xs={4}>
<IconButton onClick={onClickCreateFeed} style={{padding: 0}}>
<AddCircleIcon fontSize="large" color="primary" />
</IconButton>
</Grid>
</Grid>
)
}
export default Header;
-왼쪽 대표이미지를 누르면 "/myFeed"로 이동
-가운데 사이트 이름을 누르면 "/home"으로 이동
-오른쪽 플러스 아이콘을 누르면 "/createFeed"로 이동
3. 내 피드 네비게이션 바(상단)
-/MyFeed/MyFeedHeader.jsx
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Grid, Typography } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { signOut } from "firebase/auth";
import { getDocs, query, collection, where } from "firebase/firestore";
import { db, auth } from "../../Env/Firebase";
const MyFeedHeader = () => {
const navigate = useNavigate();
const id = sessionStorage.getItem("user_id");
const [userImage, setUserImage] = useState("");
const [anchorElNav, setAnchorElNav] = useState(null);
useEffect(() => {
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
setUserImage(doc.data().image);
});
});
}, []);
/** 세팅 메뉴 오픈 */
const handleOpenSetting = (event) => {
setAnchorElNav(event.currentTarget);
};
/** 세팅 메뉴 오프 */
const handleCloseNavMenu = () => {
setAnchorElNav(null);
};
/** 왼쪽 사진 클릭시 내 피드로 넘어가기 */
const onClickMyFeed = () => {
navigate("/myFeed");
}
/** 오른쪽 설정 아이콘 클릭시 나오는 메뉴 중 로그아웃 버튼을 누르면
* 로그아웃을 진행한 후 로그인 화면으로 넘어간다.
*/
const onClickLogout = () => {
setAnchorElNav(null);
signOut(auth).then(() => {
navigate("/");
}).catch((error) => {
alert(error.message);
});
}
return (
<Grid container style={{ alignItems: "center", textAlign: "center", width: "80%", margin: "auto", marginTop: 70, top: 0, position: "sticky" }}>
<Grid item xs={4}>
{userImage &&
<IconButton onClick={onClickMyFeed} style={{ padding: 0 }}>
<img src={userImage} style={{ width: 40, height: 40, borderRadius: "50%" }} />
</IconButton>
}
</Grid>
<Grid item xs={4}>
<Typography variant="h4">
<Link to="/home" style={{ textDecoration: 'none', color: 'black' }}>
Youngstagram
</Link>
</Typography>
</Grid>
<Grid item xs={4}>
<IconButton
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleOpenSetting}
style={{ padding: 0 }}
>
<SettingsIcon fontSize="large" color="grey" />
</IconButton>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
>
<MenuItem onClick={onClickLogout}>
<Typography variant="button">
로그아웃
</Typography>
</MenuItem>
</Menu>
</Grid>
</Grid>
)
}
export default MyFeedHeader;
4. 내 피드 페이지
-/MyFeed/MyFeed.jsx
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Grid, Typography, Button } from "@mui/material";
import { getDocs, query, collection, where } from "firebase/firestore";
import { db } from "../../Env/Firebase";
import EditHeader from "./MyFeedHeader";
const MyFeed = () => {
const navigate = useNavigate();
const id = sessionStorage.getItem("user_id");
const [data, setData] = useState();
useEffect(() => {
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
setData({
name: doc.data().name,
userImage: doc.data().image,
countFeed: doc.data().count_feed,
introduce: doc.data().introduce
})
});
});
}, []);
/** 프로필 편집 버튼 누를 시 state안의 자료형 보내면서 "/edit"으로 이동 */
const onClickEdit = () => {
navigate("/edit", {
state: {
name: data.name,
introduce: data.introduce,
userImage: data.userImage
}
});
}
return (
<>
<EditHeader />
<Grid container style={{ alignItems: "center", textAlign: "center", width: "80%", margin: "auto", marginTop: 50 }}>
<Grid item xs={6}>
{data && <img src={data.userImage} style={{ width: "50%", height: 250, borderRadius: "50%" }} />}
</Grid>
<Grid item xs={6}>
{data && data.countFeed}
<Typography>
게시물
</Typography>
</Grid>
<Grid item xs={6} style={{ marginTop: 10 }}>
<Typography>
{data && data.name}
</Typography>
</Grid>
<Grid item xs={6}>
</Grid>
<Grid item xs={6} style={{ marginTop: 10 }}>
<Typography>
{data && data.introduce}
</Typography>
</Grid>
<Grid item xs={12} style={{ marginTop: 30 }}>
<Button variant="contained" onClick={onClickEdit} style={{ width: "60%", backgroundColor: "black", color: "white" }}>
프로필 편집
</Button>
</Grid>
</Grid>
</>
)
}
export default MyFeed;
대표이미지와 게시물 수, 이름, 소개를 Firebase Firestore에서 가져온다.
5. 프로필 편집 페이지
-/MyFeed/Edit.jsx
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Box, Button, Grid, TextField, Typography } from "@mui/material";
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
import { collection, query, where, getDocs, updateDoc, doc } from "firebase/firestore";
import { storage, db } from "../../Env/Firebase";
import Loader from "../../Env/Loader";
import Header from "../Home/Header";
const Edit = () => {
const location = useLocation();
const navigate = useNavigate();
const id = sessionStorage.getItem("user_id");
const [isLoading, setIsLoading] = useState(false);
const [name, setName] = useState(location.state.name);
const [introduce, setIntroduce] = useState(location.state.introduce);
const [userImage, setUserImage] = useState(location.state.userImage);
const onChange = (event) => {
const { target: { id, value } } = event;
if (id === "name") {
setName(value);
} else if (id === "introduce") {
setIntroduce(value);
}
}
/** 프로필 사진 변경 누를 시 유저는 원하는 사진을 고른다.
* 사진을 고르면 로딩 화면이 뜨고 Firebase Storage에 사진 저장
* 사진 저장된 url을 가져와서 로딩 화면 끄고 유저에게 바뀐 사진 먼저 보여준다.
* 그 후 users 테이블에 image 필드값 업데이트
*/
const onChangeImage = (e) => {
setIsLoading(true);
const storageRef = ref(storage, `/user_image/${e.target.files[0].name}`);
uploadBytesResumable(storageRef, e.target.files[0]).then(() => {
getDownloadURL(storageRef).then((url) => {
setUserImage(url);
setIsLoading(false);
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then(querySnapshot => {
querySnapshot.forEach((document) => {
const userRef = doc(db, "users", document.id);
updateDoc(userRef, {
image: url
});
});
});
});
});
}
/** 완료 버튼 누를 시 유저가 입력한 이름과 소개를 users 테이블에 업데이트 */
const onClickComplete = () => {
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then(querySnapshot => {
querySnapshot.forEach((document) => {
const userRef = doc(db, "users", document.id);
updateDoc(userRef, {
name: name,
introduce: introduce
}).then(() => {
navigate("/myFeed");
});
});
});
}
if (isLoading) return <Loader />
return (
<>
<Header />
<Box
style={{
width: 350,
height: "90vh",
alignItems: "center",
display: "flex",
textAlign: "center",
margin: "auto"
}}
>
<Grid container>
<Grid item xs={12}>
<img src={userImage} style={{ width: "75%", height: 250, borderRadius: "50%" }} />
<Typography variant="subtitle1">
<input
type="file"
id="contained-button-file"
style={{ display: 'none' }}
onChange={onChangeImage}
/>
</Typography>
<label htmlFor="contained-button-file">
<Button color="primary" component="span">
프로필 사진 변경
</Button>
</label>
</Grid>
<Grid item xs={12} style={{ marginTop: 50 }}>
<TextField
id="name"
label="Name"
variant="outlined"
value={name}
sx={{
"& .MuiInputLabel-root": { color: 'black' },
"& .MuiOutlinedInput-root": { "& > fieldset": { borderColor: "black" } },
"& .MuiOutlinedInput-root.Mui-focused": { "& > fieldset": { borderColor: "black" } },
"& .MuiOutlinedInput-root:hover": { "& > fieldset": { borderColor: "black" } },
width: 250
}}
onChange={onChange}
/>
</Grid>
<Grid item xs={12} style={{ marginTop: 20 }}>
<TextField
id="introduce"
label="Introduce"
multiline
rows={4}
sx={{
"& .MuiInputLabel-root": { color: 'black' },
"& .MuiOutlinedInput-root": { "& > fieldset": { borderColor: "black" } },
"& .MuiOutlinedInput-root.Mui-focused": { "& > fieldset": { borderColor: "black" } },
"& .MuiOutlinedInput-root:hover": { "& > fieldset": { borderColor: "black" } },
width: 250
}}
value={introduce}
onChange={onChange}
/>
</Grid>
<Grid item xs={12} style={{ marginTop: 40 }}>
<Button
variant="contained"
style={{ width: 250, color: "white", height: 40 }}
onClick={onClickComplete}
>
<Typography variant="subtitle1">
완료
</Typography>
</Button>
</Grid>
</Grid>
</Box>
</>
)
}
export default Edit;
헤더는 홈페이지에서 보는 헤더와 같다.