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;

헤더는 홈페이지에서 보는 헤더와 같다.