App.js
import "./App.css";
import { Outlet } from "react-router-dom";
import Navbar from "./components/Navbar";
import { AuthContextProvider } from "./components/context/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<Navbar />
<Outlet />
</AuthContextProvider>
</QueryClientProvider>
);
}
export default App;
index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Playfair+Display&display=swap");
body {
margin: 0;
font-family: Noto Sans KR;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-slate-900;
}
#root {
@apply w-full text-inherit;
}
input {
@apply p-4 border border-slate-200 my-1 rounded-sm
}
input:focus{
@apply outline outline-slate-700 bg-slate-100
}
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./index.css";
import App from "./App";
import NotFound from "./pages/NotFound";
import Home from "./pages/Home";
import AllProducts from "./pages/AllProducts";
import NewProduct from "./pages/NewProduct";
import ProductDetail from "./pages/ProductDetail";
import MyCart from "./pages/MyCart";
import ProtectedRoute from "./components/ProtectedRoute";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <NotFound />,
children: [
{ index: true, path: "/", element: <Home /> },
{ path: "/products", element: <AllProducts /> },
{
// * 7. admin์ผ ๋๋ฅผ ๊ตฌ๋ถํ๋ ์กฐ๊ฑด ์ถ๊ฐtrue๋ฉด ์๋ต ๊ฐ๋ฅ)
path: "/products/new",
element: (
<ProtectedRoute requireAdmin={true}>
<NewProduct />
</ProtectedRoute>
),
},
{ path: "/products/:id", element: <ProductDetail /> },
{
path: "/cart",
element: (
<ProtectedRoute>
<MyCart />
</ProtectedRoute>
),
},
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
Navbar.jsx
import { Link } from "react-router-dom";
import { HiPencilAlt } from "react-icons/hi";
import User from "./User";
import Button from "./ui/Button";
import { useAuthContext } from "./context/AuthContext";
import CartStatus from "./CartStatus";
// // * 1-1. useState ์ ์ธ
// ๋ก๊ทธ์ธ ์ฌ๋ถ
export default function Navbar() {
// * 7-6. useAuthContext ์ฌ์ฉ
const { user, login, logout } = useAuthContext();
// const [user, setUser] = useState();
// // * 2. ํ๋ฉด์ด ๋ง์ดํธ๋ ๋(reload๋ ๋) ๋ก๊ทธ์ธ์ด ๋์ด์๋์ง ์๋์ง ์ํ๋ฅผ ์์๋ณด๋ ํจ์ ํธ์ถ
// useEffect(() => {
// onUserStateChange((user) => {
// setUser(user);
// console.log("user? : ", user); // admin์ ๋ง๋ค๊ณ ์ถ์ ์ ์ ์ uid๋ฅผ ํ์ธํ๊ธฐ ์ํด ์์ฑ
// });
// }, []);
// // * 1-2. onClick์ login ํจ์๋ฅผ ๋ฃ์ง ์๊ณ ์ด๋ ๊ฒ ์์ฑํ๋ ์ด์ ๋ firebase.js์ ์๋ user๋ฅผ ๋ฐ์์์ useState์ ์ง์ด๋ฃ๊ธฐ ์ํจ
/**
* ๋ก๊ทธ์ธํ ๋ ์ฌ์ฉ๋๋ ํจ์
*/
// ๋ฆฌํฉํ ๋ง
// const handleLogin = () => {
// login().then(setUser);
// };
// const handleLogout = () => {
// logout().then(setUser); // useState์ user๋ฅผ ๋น์ด๋ค. (null ์ํ๋ก ๋ง๋ฆ)
// };
return (
<div className="fixed w-full z-10 border-b border-slate-50/20 text-slate-500 hover:text-black hover:bg-white transition duration-500 bg-white bg-opacity-10">
<div className="w-full max-w-screen-2xl m-auto">
<header className="flex justify-between items-center p-2 md:p-5">
<Link to="/">
<h1 className="text-lg md:text-3xl font-logoFont tracking-normal md:tracking-widest">
RALPH<span className="pl-3 md:pl-6">LAUREN</span>
</h1>
</Link>
<nav className="flex items-center gap-2 md:gap-4 text-sm md:text-base">
<Link to="/products">Product</Link>
{/* cart๋ฅผ CartStatus๋ผ๋ ์ปดํฌ๋ํธ๋ก ๋ฐ๋ก ๋นผ์ ์์ฑ */}
{user && <Link to="/cart"><CartStatus /></Link>}
{/* // * 5. isAdmin์ด true์ผ ๋๋ง ๋ณด์ด๋๋ก */}
{user && user.isAdmin && (
<Link to="/products/new">
<HiPencilAlt />
</Link>
)}
{/* // *3. User.jsx - user๊ฐ ์์ ๊ฒฝ์ฐ ์คํ */}
{user && <User user={user} />}
{/*// * 1-2. */}
{/* // * 2-1. ๋ฐ๋ก ์ ์ธํ์ง ์๊ณ firebase ์์ ์๋ ํจ์๋ฅผ ๋ฐ๋ก ํธ์ถ */}
{/* // * 6. Button ์ปดํฌ๋ํธ๋ก ์ ํ */}
{!user && <Button onClick={login} text={"login"} />}
{user && <Button onClick={logout} text={"logout"} />}
</nav>
</header>
</div>
</div>
);
}
User.jsx
import React from "react";
// * 3. ์ ์ ๋ฅผ ๋ํ๋ด๋ User.jsx ๋ง๋ฆ
export default function User({ user: { displayName, photoURL } }) {
// console.log("user: ", user);
return (
// shrink : ๋ถ๋ชจ ์์ญ์ด ์ค๋ฉด ์์ item๋ค๋ ๊ฐ์ด ์ค์ด๋ฆ - shrink-0์ ๊ทธ๊ฒ์ ๋ฐฉ์งํด์ค
<div className="flex items-center shrink-0">
<img
className="w-10 h-10 rounded-full mr-2"
src={photoURL}
alt={displayName}
/>
<span className="hidden md:block">{displayName}</span>
</div>
);
}
Button.jsx
import React from "react";
export default function Button({ onClick, text }) {
return (
<button
className="bg-brand text-white py-2 px-4 rounded-sm hover:brightness-200 text-sm"
onClick={onClick}
>
{text}
</button>
);
}
firebase.js
import { initializeApp } from "firebase/app";
import {
getAuth,
signInWithPopup,
GoogleAuthProvider,
signOut,
onAuthStateChanged,
} from "firebase/auth";
import { getDatabase, get, set, ref } from "firebase/database";
import uuid from "react-uuid";
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
databaseURL: process.env.REACT_APP_FIREBASE_DB_URL,
};
const app = initializeApp(firebaseConfig);
const provider = new GoogleAuthProvider();
const auth = getAuth();
// * 1-1. ํจ์ ์ ์ธ์ firebase ์์์ ํจ
export function login() {
return signInWithPopup(auth, provider) // ์๋ ๋ฆฌํด๋ user ๊ฐ์ ๋ฐ์์์ ๊ฒฐ๊ณผ๊ฐ์ผ๋ก ๋ด๋ณด๋ด๊ธฐ ์ํด return ์์ฑ
.then((result) => {
// ๋ก๊ทธ์ธ ๋์๋์ง ๊ฒฐ๊ณผ๋ฅผ ์ป์ด์ด
const user = result.user;
// console.log("user? : ", user);
return user;
})
.catch(console.error);
}
export async function logout() {
return signOut(auth) // null๊ฐ์ด ๋ฆฌํด๋จ
.then(() => null); // null
// .catch((error) => {});
}
// * 2. ํ๋ฉด์ด ๋ง์ดํธ๋ ๋(reload๋ ๋) ๋ก๊ทธ์ธ์ด ๋์ด์๋์ง ์๋์ง ์ํ๋ฅผ ์์๋ณด๋ ํจ์ ์ ์ธ (callback)
export function onUserStateChange(callback) {
// ๋ง์ฝ user๊ฐ ์์ ๊ฒฝ์ฐ
onAuthStateChanged(auth, async (user) => {
// ? 1. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ๊ฒฝ์ฐ
// user && adminUser(user);
const updatedUser = user ? await adminUser(user) : null;
callback(updatedUser);
});
}
// * 4. ์ค์๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ ์ ์ธ
const database = getDatabase(app);
// ? 2. ์ฌ์ฉ์๊ฐ ์ด๋๋ฏผ ๊ถํ์ด ์๋์ง ํ์ธ -> isAdmin์ user ์์ ๋ฃ์
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์๋ admins๋ฅผ ์ฐธ์กฐํ๋ ํจ์
async function adminUser(user) {
// database ์์ admins key๊ฐ ์์
return (
get(ref(database, "admins")) // ref(database์ด๋ฆ, key๊ฐ)
// ๋ง์ฝ snapshot(๊ฒฐ๊ณผ๊ฐ)์ด ์กด์ฌํ๋ฉด
.then((snapshot) => {
if (snapshot.exists()) {
const admins = snapshot.val(); // snapshot์ value
// admins๊ฐ user.uid๋ฅผ ํฌํจํจ
const isAdmin = admins.includes(user.uid);
return { ...user, isAdmin }; // ๋ง์ user๋ค์ ํญ๋ชฉ ์ค์์ isAdmin๋ง ๋ผ์๋ฃ์
}
return user;
})
);
}
//์ ํ๋ฑ๋ก
export async function addNewProduct(product, image) {
const id = uuid();
console.log(id);
return set(ref(database, `products/${id}`), {
...product,
id,
price: parseInt(product.price),
options: product.options.split(","),
image,
});
}
//์ ํ๊ฐ์ ธ์ค๊ธฐ
export async function getProduct() {
return get(ref(database, "products")).then((snapshot) => {
// exists : ์กด์ฌํ ๋๋ง value๊ฐ ๋ถ๋ฌ์ด
if (snapshot.exists()) {
return Object.values(snapshot.val());
}
});
}
// ์ฌ์ฉ์์ ์นดํธ์ ์ถ๊ฐํ๊ฑฐ๋ ์
๋ฐ์ดํธ (์ ํ๋ฑ๋ก๊ณผ ๋น์ทํ ๋ก์ง)
export async function addOrUpdateToCart(userId, product) {
return set(ref(database, `carts/${userId}/${product.id}`), product);
}
// ํน์ ์ฌ์ฉ์์ ์ฅ๋ฐ๊ตฌ๋(cart)๋ฅผ ๊ฐ์ ธ์ด
export async function getCart(userId) {
// products๋ง ๋ถ๋ฌ์ค๋๊ฒ ์๋ carts์์ ํน์ id๋ฅผ ๊ฐ์ ธ์์ผ ํจ
return get(ref(database, `carts/${userId}`))
.then((snapshot) => {
const items = snapshot.val() || {};
return Object.values(items);
});
}
/*
1. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ๊ฒฝ์ฐ
2. ์ฌ์ฉ์๊ฐ ์ด๋๋ฏผ ๊ถํ์ด ์๋์ง ํ์ธ
3. ์ฌ์ฉ์์๊ฒ ์๋ ค์ค
*/
uploader.js
// https://console.cloudinary.com/documentation/upload_images
// * 11. uploadImage ํจ์ ์ ์ธ (cloudinary์ ์ฌ๋ผ๊ฐ)
export async function uploadImage(file) {
const data = new FormData();
const url = process.env.REACT_APP_CLOUDINARY_URL;
// ํ์ผ์ด ํ๋์ด๊ธฐ ๋๋ฌธ์ for๋ฌธ ํ์ ์์
data.append("file", file);
data.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET);
return fetch(url, {
method: "POST",
body: data
})
.then((res) => res.json())
.then((data) => data.url)
}
๋ฉ์ธ ํ๋ฉด
๋ก๊ทธ์ธ ํ ์ ์ ์ ์ด๋ฆ๊ณผ ํ๋กํ ์ฌ์ง์ด ๋ณด์ฌ์ง(๊ฐ๋ ค๋์ ๋ถ๋ถ)
์ฅ๋ฐ๊ตฌ๋์ ๋ด์ item๋ค์ ์ฅ๋ฐ๊ตฌ๋ ํญ์์ ๋ณด์ฌ์ค
์๋ ํ์ธ์. ๋๋ฆผ์ฝ๋ฉ ์ด์์ ์ ๋๋ค.
์๊ฐ์๋ถ์ด ๋ฐ๊ฒฌํ์ฌ ์ ๊ณ ๊ฐ ๋ค์ด์ ๋ธ๋ก๊ทธ์ ๋ํด ์๊ฒ ๋์์ต๋๋ค.
ํด๋น ๊ฒ์๊ธ๊ณผ ๋ธ๋ก๊ทธ์ ์ฌ๋ฆฌ์ ๋ค์์ ๊ธ๋ค์ด ๋๋ฆผ์ฝ๋ฉ ์์นด๋ฐ๋ฏธ ์ ๋ฃ ๊ฐ์์ ๋ด์ฉ์ ์ ๋ฆฌ ํ์ ๊ฑธ๋ก ํ์ธ๋ฉ๋๋ค.
์ด๋ ์์ฐํ ์ ์๊ถ๋ฒ ์๋ฐ์ ๋๋ค.
ํด๋น ํฌ์คํธ๋ค์ ๋น๊ณต๊ฐ ๋๋ ์ญ์ ์ฒ๋ฆฌํ info@dream-coding.com ๋ก ๋ฉ์ผ ๋ถํ๋๋ฆฝ๋๋ค.
์์ผ๋ด์ ์ฒ๋ฆฌํ์ง ์์ผ์๋ฉด ๊ฐ์ ์ทจ์ ๋ฐ ๋ฒ์ ๋์ ํ๊ฒ ์ต๋๋ค.
๊ฐ์ ์์์ ์ ์๊ถ๋ฒ์ ๊ด๋ จํด์ ๋ธ๋ก๊ทธ์ ์ ๋ฆฌํ์ง ๋ง์ ๋ฌ๋ผ๊ณ ์๋ดํด ๋๋ ธ์ต๋๋ค.
https://academy.dream-coding.com/courses/player/react/lessons/1462