App.js
import { useState } from 'react';
import Cart from './components/Cart/Cart';
import Header from './components/Layout/Header';
import Meals from './components/Meals/Meals';
import CartProvider from './store/CartProvider';
import './App.css';
function App() {
const [cartInShow, setCartInShow] = useState(false);
//cart๋ชจ๋ฌ์ฐฝ์ด ๋ณด์ด๊ณ ์๋ณด์ด๊ณ , ์ํ๊ด๋ฆฌ
//cart๋ชจ๋ฌ ๋ณด์ด๊ฒ ํ๋ ํจ์
const showCartHandler=()=>{
setCartInShow(true)
}
//cart๋ชจ๋ฌ ์๋ณด์ด๊ฒ ํ๋ ํจ์
const hideCartHandler=()=>{
setCartInShow(false)
}
return (
// CartProvider ํ๊ทธ ์์์ cart-context์ ๋ด์ฉ์ ์ ๋ถ ๊ฐฑ์ /์ญ์ /์ถ๊ฐ ํ ์ ์์
<CartProvider>
{ cartInShow && <Cart onClose ={hideCartHandler} />}
{/* cartInShow๊ฐ true์ผ๋๋ง ๋ชจ๋ฌ์ด ๋ณด์ */}
<Header onShowCart={showCartHandler}/>
<main>
<Meals />
</main>
</CartProvider>
);
}
export default App;
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>์ข
ํฉ์์ </title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- portal ์ง์ -->
<div id="overlay"></div>
<div id="root"></div>
</body>
</html>
Header.jsx
import React from 'react'
import classes from "./Header.module.css";
import mealsImg from "../../asset/meals.jpg";
import HeaderCartButton from './HeaderCartButton';
//public์ ๋๊ณ ์ผ๋ฐ ์นํ์ด์ง์์ ์ด๋ฏธ์ง ์ฐ๋ฏ์ด ์ฌ์ฉํ ์๋ ์์
const Header = (props) => {
return (
<>
<header className={classes.header}>
<h1>First React Meals</h1>
{/* onclick : props ์ด๋ฆ */}
<HeaderCartButton onclick={props.onShowCart} />
</header>
<div className={classes["main-image"]}>
{/* main-image ํด๋ผ์ค๋ ์์ -๊ฐ ์์ด์ .์ ๋ชป ์ */}
<img src={mealsImg} alt="meals" />
</div>
</>
)
}
export default Header;
HeaderCartButton.jsx
import React, { useContext, useEffect, useState } from "react";
import CartIcon from "../Cart/CartIcon";
import classes from "./HeaderCartButton.module.css";
import CartContext from "../../store/cart-context";
// useEffect() : ์ด๋ค ์ผ์ด ๋ฐ์ํ ๋๋ง๋ค ์คํ๋๋ React hook
const HeaderCartButton = (props) => {
const cartCtx = useContext(CartContext);
// const numberOfCartItems = cartCtx.items.length; // = ์์ดํ
์ซ์๋ฅผ ์๋ฏธ -> ์์ดํ
์์ amount๋ฅผ ํฉํด์ค์ผ ํจ
const [btnIsHigh, setBtnIsHigh] = useState(false); // ๋ฒํผ ์ํ(์ ๋๋ฉ์ด์
์ ์ฉ ์ฌ๋ถ)
const { items } = cartCtx; // ๊ตฌ์กฐ๋ถํด
// ์ปจํ
์คํธ์ ๋ฐฐ์ด์ด ๋ฐ๋ ๋ ์ ์ฉ
useEffect(() => {
// ํญ๋ชฉ์ด ์์ ๋
if (items.length === 0) {
return;
}
setBtnIsHigh(true);
const timer = setTimeout(() => {
setBtnIsHigh(false);
}, 300);
// ์ฌ์ด๋์ดํํธ ์ ๋ฆฌ, ํด๋ฆฐ์
ํจ์ (๋์ ๋ณด์ด์ง ์์ง๋ง ์ฝ๋์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊น๋ํ๊ฒ ์ ๋ฆฌํจ.)
return () => {
clearTimeout(timer);
};
}, [items]);
// ์์ดํ
์ ๋ณํ๊ฐ ์์ ๋๋ง bump ํด๋์ค ๋ค์ด๊ฐ
const btnClass = `${classes.button} ${btnIsHigh ? classes.bump : ""}`;
// ์์์ ๊ตฌ์กฐ๋ถํด ํ๊ธฐ ๋๋ฌธ์ cartCtx.items๋ผ๊ณ ์ ์จ๋ ๋จ
const numberOfCartItems = items.reduce((sum, item) => {
return (sum += item.amount);
}, 0);
// ๋ฐฐ์ด.reduce((ํฉํด์ง ๊ฐ, value) => { ํฉํด์ง๊ฐ + ๋ฐธ๋ฅ }, ํฉํด์ง ๊ฐ์ ์ด๊ธฐ๊ฐ);
return (
<button className={btnClass} onClick={props.onclick}>
<span className={classes.icon}>
<CartIcon />
</span>
<span>Your Cart</span>
<span className={classes.badge}>{numberOfCartItems}</span>
</button>
);
};
export default HeaderCartButton;
Card.jsx
import React from 'react'
import classes from "./Card.module.css";
const Card = (props) => {
return (
<div className={classes.card}>{props.children}</div>
)
}
export default Card
input.jsx
import React from "react";
import classes from "./Input.module.css";
const Input = props => {
return (
<div className={classes.input}>
<label htmlFor={props.input.id}>{props.label}</label>
<input ref={props.propsRef} {...props.input} />
</div>
);
};
export default Input;
/*
forwardRef()
- ์ ๋ฌ๋ฐ์ ref ์ดํธ๋ฆฌ๋ทฐํธ๋ฅผ ํ๋ถ ํธ๋ฆฌ ๋ด์ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ก ์ ๋ฌํ ๋ ํจ์ ๋ถ๋ถ์ ๊ฐ์ธ์ ์ฌ์ฉ
*/
Modal.jsx
//๋ชจ๋ฌ ํ์
์ฐฝ
import React from 'react'
import ReactDOM from 'react-dom';
import classes from './Modal.module.css'
//๋ชจ๋ฌ๋ค ๊น๋ง ๋ฐํฌ๋ช
const Backdrop=(props)=>{
return <div className={classes.backdrop} onClick={props.onClose} > </div>
}
//์ค์ ๋ชจ๋ฌ(๊ฐ์ด๋ฐ ํ์๋ค๋ชจ)
const ModalOverlay=(props)=>{
return (
<div className={classes.modal}>
<div>{props.children}</div>
</div>
)
};
// portal์ ์ถ๋ ฅํ ์์น๋ฅผ ๊ฐ์ ธ์ด
const portalElement = document.getElementById('overlay');
//๋ฉ์ธ ์ปดํฌ๋ํธ
const Modal = (props) => {
return (
<div>
{/* {ReactDOM.createPortal(<Backdrop onClose={props.onClose} />,portalElement)}
{ReactDOM.createPortal(<ModalOverlay>{props.children}</ModalOverlay>, portalElement )} */}
{ReactDOM.createPortal(<Backdrop onClose={props.onClose} />, portalElement)}
{ReactDOM.createPortal(<ModalOverlay>{props.children}</ModalOverlay>, portalElement)}
</div>
)
}
export default Modal;
// createPortal(child(์์์์), container(ํฌํ์ด๋ฆ))
Meals.jsx
import React from 'react'
import AvailableMeals from './AvailableMeals'
import MealsSummary from './MealsSummary'
const Meals = () => {
return (
<>
<MealsSummary />
<AvailableMeals />
</>
)
}
export default Meals
MealsSummary.jsx
import React from "react";
import classes from "./MealsSummary.module.css";
const MealsSummary = () => {
return (
<section className={classes.summary}>
{/* <h2>MealsSummary.js --</h2> */}
<p>
Choose your favorite meal from our broad selection of available meals
and enjoy a delicious lunch or dinner at home.
</p>
<p>
All our meals are cooked with high-quality ingredients, just-in-time and of course by experienced
chefs!
</p>
</section>
);
};
export default MealsSummary;
AvailableMeals.jsx
import React, { useEffect, useState } from "react";
import Card from "../UI/Card";
import classes from "./AvailableMeals.module.css";
import MealItem from "./MealsItem/MealItem";
// const DUMMY_MEALS = [
// {
// id: "m1",
// name: "Sushi",
// description: "Finest fish and veggies",
// price: 22.9998,
// },
// {
// id: "m2",
// name: "Schnitzel",
// description: "A german specialty!",
// price: 16.5,
// },
// {
// id: "m3",
// name: "Barbecue Burger",
// description: "American, raw, meaty",
// price: 12.991,
// },
// {
// id: "m4",
// name: "Green Bowl",
// description: "Healthy...and green...",
// price: 18.993,
// },
// ];
const AvailableMeals = () => {
const [meals, setMeals] = useState([]); // ๋ฐฑ์๋์ ์ ์ฅ๋์ด ์๋ ๋ฉ๋ด
const [isLoading, setIsLoading] = useState(true); // ํ๋ฉด์ด ์ด๋ฆฌ๋ฉด ๋ฌด์กฐ๊ฑด ๋ก๋ฉํ๋ฉด์ด ๋จ๋๋ก true ์ค์
const [httpError, setHttpError] = useState(); // ์๋ฌ ๋ฌธ๊ตฌ๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ์ํ ์ค์
useEffect(()=>{
// ๋น๋๊ธฐ ํจ์๋ ๋ฆฌํด๊ฐ์ด promise๋ก ๋์ด
const fetchMeals = async () =>{
const response = await fetch('https://meal-2269f-default-rtdb.asia-southeast1.firebasedatabase.app/meals.json');
// fetch์ ๊ฒฐ๊ณผ๋ฌผ(๋ฆฌํด๊ฐ) = response.ok = response๊ฐ true
if (!response.ok) {
throw new Error("์๋ฌ ๋ฐ์๐ฅ");
}
const data = await response.json() //๊ฐ์ฒด ํ์์ผ๋ก ์ ์ฅ๋จ
console.log('data?',data)
const loadMeals = [];
for (const key in data) {
loadMeals.push({
id: key,
name: data[key].name,
description: data[key].description,
price: data[key].price
})
}
setMeals(loadMeals);
setIsLoading(false);
}
/*
try {
fetchMeals();
} catch (error) {
setIsLoading(false);
setHttpError(error.message);
}
*/
// fetchMeals๋ async๋ฅผ ์ฌ์ฉ -> promise๋ฅผ ๋ฐํ -> ์๋ฌ (useEffect ์์์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๋ฐ์)
// ํด๊ฒฐ๋ฐฉ๋ฒ - ๋ณ๋์ ํจ์๋ฅผ ํ ๊ฐ ๋ ์ถ๊ฐ or ์๋ ๋ฐฉ๋ฒ
fetchMeals().catch((error) => {
setIsLoading(false);
setHttpError(error.message);
});
// fetchMeals();
}, [])
// console.log("meals๋? : ", meals);
// ๋ก๋ฉ์ฒ๋ฆฌ
if(isLoading){
return(
<section className={classes.mealsLoading}>
<p>Loading....</p>
</section>
)
}
// ์๋ฌ์ฒ๋ฆฌ
if(httpError) {
return(
<section className={classes.mealsError}>
<p>{httpError}</p>
</section>
)
}
const mealsList = meals.map((meal)=>
<MealItem
key={meal.id}
id={meal.id}
name={meal.name}
description={meal.description}
price={meal.price}/>
)
return (
<section className={classes.meals}>
<Card>
<ul>
{mealsList}
</ul>
</Card>
</section>
)
}
export default AvailableMeals;
MealItem.jsx
import React, { useContext } from "react";
import classes from "./MealItem.module.css";
import MealItemForm from "./MealItemForm";
import CartContext from "../../../store/cart-context";
const MealItem = (props) => {
const cartCtx = useContext(CartContext);
const price = `$${props.price.toFixed(2)}`;
// toFixed(n) : ์์์ n๋ฒ์งธ ์๋ฆฌ๊น์ง๋ง ์ถ๋ ฅ(๋ฐ์ฌ๋ฆผ). ์ฒซ ๋ฒ์งธ ์๋ฆฌ๊น์ง๋ง ์์ผ๋ฉด ๋ ๋ฒ์งธ ์๋ฆฌ๋ 0์ผ๋ก ์ถ๋ ฅ
// ํจ์ ์ ์
// context์ ์ ๋ฌํ๋ ํจ์ (amount : ์๋)
// ์ธ์๊ฐ์ MealItemForm์์ value๋ฅผ ๋ฐ์์ด
const addToCartHandler = (amount) => {
console.log("์๋์? : ", amount);
// ์ฌ๊ธฐ์ addItem์ ๊ฐ์ ธ์์ ์๋์ํด
cartCtx.addItem({
id: props.id,
name: props.name,
amount: amount, // amount๋ก ๋ฐ์์์
price: props.price,
});
};
return (
<li className={classes.meal}>
<div>
<h3>{props.name}</h3>
<div className={classes.description}>{props.description}</div>
<div className={classes.price}>{price}</div>
</div>
<div>
{/* mealitem์์ mealitemform์ผ๋ก ๋ณด๋ (props) */}
<MealItemForm id={props.id} onAddToCart={addToCartHandler} />
</div>
</li>
);
};
export default MealItem;
MealItemForm.jsx
import React, { useRef } from "react";
import Input from "../../UI/Input";
import classes from "./MealItemForm.module.css";
const MealItemForm = (props) => {
// ref๋ฅผ ํตํด์ ์
๋ ฅ๋ ๊ฐ์ ๋ฐ์์ด(ํน์ DOM์ ์ ํํ ๋ ์ฌ์ฉ)
const amountInputRef = useRef();
const submitHandler = (event) => {
event.preventDefault();
const enteredAmount = amountInputRef.current.value; // string
// const enteredAmountNumber = Number(enteredAmount); // number
const enteredAmountNumber = +enteredAmount; // number (์ซ์์ด๋ก ๋ณ๊ฒฝ)
// console.log(typeof enteredAmountNumber);
// ์๋์ onAddToCart์ ์ธ์๊ฐ์ผ๋ก ๋ฐ์ Mealitem์ ๋๊ฒจ์ค
props.onAddToCart(enteredAmountNumber);
/*
// ์ ํจ์ฑ ๊ฒ์ฌ
// ๊ณต๋ฐฑ์ด ์๋๊ฐ
if (enteredAmount.trim().length === 0 || enteredAmount < 1 || enteredAmount > 5) {
return;
}
*/
};
return (
<form className={classes.form} onSubmit={submitHandler}>
<Input
label="Amount"
propsRef={amountInputRef} // props
input={{
id: "amount_" + props.id,
type: "number",
min: "1",
max: "5",
defaultValue: "1",
step: "1",
}}
/>
{/* <input
type="number"
min="1"
max="5"
defaultValue="1"
step="1"
id={props.id}
/> */}
<button>+ Add</button>
</form>
);
};
export default MealItemForm;
Cart.jsx
//๋ชจ๋ฌํ์
์์ ๋ค์ด๊ฐ ๋ด์ฉ
import React, { useContext, useState } from "react";
import Modal from "../UI/Modal";
import classes from "./Cart.module.css";
import CartContext from "../../store/cart-context";
import Checkout from "./Checkout";
const Cart = (props) => {
const [isCheckout, setIsCheckout] = useState(false); // ์ฃผ๋ฌธ์ ์ฃผ์ ๋ณด์ด๊ฒ/์๋ณด์ด๊ฒ ํ๋ state
const [isSubmitting, setIsSubmitting] = useState(false); // ์ ์ถ์ค ํ์
const [didSubmit, setDidSubmit] = useState(false) // ์ ์ถ ์๋ฃ
const cartCtx = useContext(CartContext);
const hasItems = cartCtx.items.length > 0; // ์ฅ๋ฐ๊ตฌ๋์ ํญ๋ชฉ์ด ์์ ๊ฒฝ์ฐ
const totalAmount = `$ ${cartCtx.totalAmount.toFixed(2)}`;
const cartItemRemoveHandler = (id) => {
cartCtx.removeItem(id);
};
const cartItemAddHandler = (item) => {
cartCtx.addItem({ ...item, amount: 1 });
};
// orderHandler ํจ์ ์ ์
// order ๋ฒํผ์ ํด๋ฆญํ๋ฉด setIsCheckout์ด ๋ฐ๋๋์ด input์ฐฝ์ด ๋ณด์ด๋๋ก ํจ
const orderHandler = () => { setIsCheckout(true); };
// submitOrderHandler ํจ์ ์ ์
// ์
๋ ฅํ ๋ฐ์ดํฐ๋ฅผ firebase์ ๋ณด๋ด๋ ํจ์
const submitOrderHandler = async (userData) => { // property
setIsSubmitting(true);
// Cart.jsx์์ props๋ก input์ ์
๋ ฅํ ๊ฐ ๋ฐ์์ด
// console.log("userData : ", userData);
// console.log("cartCtx.items", cartCtx.items);
const response = await fetch("https://meal-57688-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json", {
// restAPI
method: "POST",
body: JSON.stringify({
user: userData,
orderItems: cartCtx.items
})
})
setIsSubmitting(false);
setDidSubmit(true);
cartCtx.clearCart(); // ์ ์ถ ์๋ฃ ํ ์นดํธ ๋น์์ค (cartCtx๋ก ๋ฐ์์ด)
};
// ๊ณ์ ๋ณํํ๋ ๊ฐ
const cartItem = (
<ul className={classes["cart-items"]}>
{/* li๋ฅผ map์ผ๋ก ๋๋ฆผ */}
{cartCtx.items.map((item) => (
<li key={item.id}>
<div>
<h2>{item.name}</h2>
<div>
<span className={classes.price}>{`$${item.price.toFixed(
2
)}`}</span>
<span className={classes.amount}>x {item.amount}</span>
</div>
</div>
<div className={classes.btns}>
<button onClick={() => cartItemRemoveHandler(item.id)}>-</button>
<button onClick={() => cartItemAddHandler(item)}>+</button>
</div>
</li>
))}
</ul>
);
// ๋ฒํผ ๋ถ๋ถ์ ๊ฐ์ ธ์์ ์์๋ก
const modalActions = (
<div className={classes.action}>
<button className={classes["button-outline"]} onClick={props.onClose}>
Close
</button>
{/* hasItems๊ฐ true์ผ ๋๋ง Order๋ฒํผ์ด ๋ณด์ด๊ฒ */}
{hasItems && (
<button className={classes.button} onClick={orderHandler}>
Order
</button>
)}
</div>
);
// Modal ์์ ๋ค์ด๊ฐ ๋ด์ฉ์ ์์๋ก ๋ง๋ฆ
const cartModalContent = (
<div>
{cartItem}
<div className={classes.total}>
<span>Total Amount</span>
<span>{totalAmount}</span>
</div>
{/* Order ๋ฒํผ์ ํด๋ฆญํ์ ๋๋ง ๋ํ๋๊ฒ */}
{isCheckout && (
<Checkout onClick={props.onClose} onConfirm={submitOrderHandler} />
)}
{!isCheckout && modalActions}
</div>
)
// ์ ์ถํ๋ ์ค์ผ ๋ ํ์๋๋ ๋ด์ฉ
const isSubmittingModalContent = (<p className={classes.submitting}>์ฃผ๋ฌธ์ค์
๋๋ค.</p>);
// ์ ์ถ ์๋ฃ ํ ํ์๋๋ ๋ด์ฉ
const didSubmitModalContent = (
<div>
<p className={classes.didSubmit}>์ฃผ๋ฌธ์ ์๋ฃํ์ต๋๋ค.</p>
<div className={classes.action}>
<button className={classes["button-outline"]} onClick={props.onClose}>
Close
</button>
</div>
</div>
);
return (
// ๋ชจ๋ฌ ์์ ๋ชจ๋ ๋ด์ฉ์ ์ง์ด๋ฃ์
<Modal onClose={props.onClose}>
{/* ์ ์ถ์ค */}
{isSubmitting && isSubmittingModalContent}
{/* ์ ์ถ ์๋ฃ */}
{/* cartModalContent๊ฐ ๋ณด์ผ ๋๋ ์๋ฃ ๋ฒํผ์ด ์์ด์ผ ํจ */}
{!isSubmitting && !didSubmit && cartModalContent}
{!isSubmitting && didSubmit && didSubmitModalContent}
</Modal>
);
};
export default Cart;
CartIcon.jsx
import React from 'react'
const CartIcon = () => {
return (
<svg fill="#fff" viewBox="0 0 576 512" width="20px" height="20px" xmlns="http://www.w3.org/2000/svg"><path d="M463.1 416c-26.51 0-47.1 21.49-47.1 48s21.49 48 47.1 48s47.1-21.49 47.1-48S490.5 416 463.1 416zM175.1 416c-26.51 0-47.1 21.49-47.1 48S149.5 512 175.1 512s47.1-21.49 47.1-48S202.5 416 175.1 416zM569.5 44.73c-6.109-8.094-15.42-12.73-25.56-12.73H121.1L119.6 19.51C117.4 8.19 107.5 0 96 0H23.1C10.75 0 0 10.75 0 23.1S10.75 48 23.1 48h52.14l60.28 316.5C138.6 375.8 148.5 384 160 384H488c13.25 0 24-10.75 24-23.1C512 346.7 501.3 336 488 336H179.9L170.7 288h318.4c14.29 0 26.84-9.47 30.77-23.21l54.86-191.1C577.5 63.05 575.6 52.83 569.5 44.73z"/></svg>
)
}
export default CartIcon
cart-context.js
import React from "react";
// ์ปจํ
์คํธ๋ฅผ ๋ง๋ฆ, ์ปจํ
์คํธ ์์ ๋ฐ์ดํ๊ฐ ์์ (์ด๊ธฐํ)
const CartContext = React.createContext({
items: [], // ์์ดํ
์ด ๋ค์ด์๋ ๋ฐฐ์ด
totalAmount: 0, // ์ด ๊ธ์ก
addItem: (item) => {},
removeItem: (id) => {},
clearCart: () => {}
});
// ์ฅ๋ฐ๊ตฌ๋ ํญ๋ชฉ ์ด๊ธฐ๊ฐ
export default CartContext;
CartProvider.jsx
import React, { useReducer } from "react";
import CartContext from "./cart-context";
// CartContext = cart-context.js ์์ 4๊ฐ์ง ๋ฐ์ดํฐ (์ด๊ธฐํ)
// reducer ํจ์ ์ ์
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedTotalAmount =
state.totalAmount + action.item.price * action.item.amount;
// ๊ธฐ์กด์ totalAmount + ๋ด๊ฐ ๊ฐ์ ธ์จ item์ ๊ฐ๊ฒฉ * ๋ด๊ฐ ๋ณด๋ธ ์์ดํ
์
// * ๊ฒ์ฌ
// state.item - ๊ธฐ์กด ๋ฐฐ์ด / Array.findIndex() - ์ ์ผ ๋จผ์ ๋์ค๋ ์กฐ๊ฑด์ ๋ง๋ ์์ดํ
์ ์ธ๋ฑ์ค(์์) ๋ฐํ
// item์ id์ ์ก์
์ผ๋ก ์ฐพ์์จ item์ id
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
); // ์ซ์๋ง ๋ฆฌํด
const existingCartItem = state.items[existingCartItemIndex]; // item ์์ฒด๋ฅผ ๋ฆฌํด
// ๊ธฐ์กด์
// console.log("๊ธฐ์กด์ ๋์ผํ ์์ดํ
์ด ์๋๊ฐ? : ", existingCartItem);
let updatedItems;
// ์ถ๊ฐํ ์์ดํ
์ด ๊ธฐ์กด์ ์๋ ์์ดํ
์ผ ๊ฒฝ์ฐ
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
updatedItems = [...state.items]; // ๊ธฐ์กด์ ๊ฐ์ฒด๋ฅผ ์ ๋ฐฐ์ด๋ก
updatedItems[existingCartItemIndex] = updatedItem; // ๊ฐ์ ๋ํด์ค ๊ธฐ์กด ์์ดํ
์
๋ฐ์ดํธ
} else {
// ์ถ๊ฐํ ์์ดํ
์ด ๊ธฐ์กด์ ์๋ ์์ดํ
์ผ ๊ฒฝ์ฐ
updatedItems = state.items.concat(action.item);
}
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
if (action.type === "REMOVE") {
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.id
);
const existingCartItem = state.items[existingCartItemIndex]; //๊ธฐ์กด์์ดํ
์ด ์์ ๊ฒฝ์ฐ๋ undefined
// console.log("๊ธฐ์กด์ ๋์ผํ ์์ดํ
์ด ์๋๊ฐ? : ", existingCartItem);
const updatedTotalAmount = state.totalAmount - existingCartItem.price;
let updatedItems;
// ์ถ๊ฐํ ์์ดํ
์ด ๊ธฐ์กด์ ์๋ ์์ดํ
์ผ ๊ฒฝ์ฐ
if (existingCartItem.amount === 1) {
// 1์ธ ์ํ์์ ๋นผ์ฃผ๋ฉด ์์ ํ ์ฌ๋ผ์ ธ์ผ ํจ
updatedItems = state.items.filter((item) => item.id !== action.id);
} else {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount - 1,
};
updatedItems = [...state.items]; // ๊ธฐ์กด์ ๊ฐ์ฒด๋ฅผ ์ ๋ฐฐ์ด๋ก
updatedItems[existingCartItemIndex] = updatedItem; // ๊ฐ์ ๋ํด์ค ๊ธฐ์กด ์์ดํ
์
๋ฐ์ดํธ
}
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
// switch (action.type) {
// case "ADD":
// return {
// items: state.items.concat(action.item), // ?
// totalAmount: state.items.totalAmount + action.item.price * action.item.amount, // ?
// }
// }
// *์ฃผ๋ฌธ ์ ์ถ ํ cart๋ฅผ ๋น์์ฃผ๋ action ์ถ๊ฐ (์ ์จ๋ ์๋ ์๋ return defaultCartState๊ฐ ์๋๋จ)
if (action.type === "CLEAR") {
return defaultCartState
}
return defaultCartState;
};
// reducer ์ด๊ธฐํ ์ ์
const defaultCartState = {
// ์๋ ๋ ๊ฐ์ ์ํ ๊ด๋ฆฌ(addItemToCartHandler๋ก ์ํ๊ด๋ฆฌ)
items: [], // ์์ดํ
์ด ๋ค์ด์๋ ๋ฐฐ์ด
totalAmount: 0, // ์ด ๊ธ์ก
};
// 2
const CartProvider = (props) => {
// useReducer ํธ์ถ(์ ์ธ)
// dispatchCartAction์ cartReducer ํจ์์ action์ผ๋ก ๋ณด๋ด๋ ์ญํ ์ ํจ
const [cartState, dispatchCartAction] = useReducer(cartReducer, defaultCartState);
// 2-1
const addItemToCartHandler = (item) => {
// dispatchCartAction์ผ๋ก ์์๋ค์ ๊ฐ์ฒด ํ์์ผ๋ก ๋ณด๋ (ํ์
๊ณผ ํ์ฌ ์์ดํ
)
// dispatchCartAction์ cartReducer ํจ์์ action์ผ๋ก ๋ณด๋ด๋ ์ญํ ์ ํจ
dispatchCartAction({ type: "ADD", item: item });
};
const removeItemFromCartHandler = (id) => {
dispatchCartAction({ type: "REMOVE", id: id });
};
// cart๋ฅผ ์ง์์ฃผ๋ ํจ์
const clearCartHandler = () => {
dispatchCartAction({type: "CLEAR"});
}
// 1
// ์
๋ฐ์ดํธ ๋ ๊ฐ์ฒด - ๋ค์ด๋๋ฏนํ๊ฒ ๋ณํ๋ ๋ถ๋ถ(๊ฒ์ ๋ณํ๋ ๋ถ๋ถ)
// items, totalAmount ๋ ๊ฐ๋ฅผ ์ ์ฅํ๋ ๊ฒ์ด ๋ชฉ์ ์. ๋๋จธ์ง๋ ํจ์ ์ ์
const cartContext = {
items: cartState.items, // ์์ดํ
์ด ๋ค์ด์๋ ๋ฐฐ์ด
totalAmount: cartState.totalAmount, // ์ด ๊ฐฏ์
addItem: addItemToCartHandler,
removeItem: removeItemFromCartHandler,
clearCart: clearCartHandler
};
return (
// 1-1
<CartContext.Provider value={cartContext}>
{props.children}
</CartContext.Provider>
);
};
export default CartProvider;
Checkout.jsx
import React, { useRef, useState } from "react";
import classes from "./Checkout.module.css";
// ์
๋ ฅ์์ ์ ํจ์ฑ ๊ฒ์ฌ
const isEmpty = (value) => value.trim() === ""; // ๋ด์ฉ์ด ๋น์ด์๋์ง ํ์ธ
const isElevenChar = (value) => value.trim().length === 11; // 11๊ธ์์ธ์ง
const Checkout = (props) => {
// ๋ค์ด๊ฐ ๋ด์ฉ๋ค ์ ์(์ค๋ธ์ ํธํํ)
const [formInputsValidity, setformInputsValidity] = useState({
name: true,
address: true,
tel: true
});
const nameInputRef = useRef();
const addressInputRef = useRef();
const telInputRef = useRef();
// console.log("useRef๋? : ", nameInputRef.current);
// ์
๋ ฅํ ๋ฐ์ดํฐ๋ฅผ ๋ฐฑ์๋์ ๋๊ฒจ์ฃผ๋ ํจ์
const confirmHandler = (e) => {
e.preventDefault();
// ์
๋ ฅ๊ฐ์ ๋ฐ์์ค๋ ๋ณ์ ์ ์
const enteredName = nameInputRef.current.value;
const enteredAddress = addressInputRef.current.value;
const enteredTel = telInputRef.current.value;
// ๊ฐ input์ด ๋น์ด์์ง ์์์ ํ์ธ
const enteredNameValid = !isEmpty(enteredName);
const enteredAddressValid = !isEmpty(enteredAddress);
const enteredTelValid = !isEmpty(enteredTel) && isElevenChar(enteredTel) && Number(enteredTel);
// ํ๋๋ผ๋ ๋น์ด์๋ input์ด ์์ผ๋ฉด false
const formIsValid = enteredNameValid && enteredAddressValid && enteredTelValid; // formIsValid = true
setformInputsValidity({
name: enteredNameValid,
address: enteredAddressValid,
tel: enteredTelValid
})
// ๊ฒ์ฆ
if (!formIsValid) { return }
// ๊ฒ์ฆ ํ ๋ฐ์ดํฐ๋ฅผ Cart.jsx๋ก ๋๊ฒจ์ค
props.onConfirm({
name: enteredName,
address: enteredAddress,
tel: enteredTel
})
}
const addressControlClass = `${classes.control} ${formInputsValidity.address? "" : classes.invalid}`;
const telControlClass = `${classes.control} ${formInputsValidity.tel? "" : classes.invalid}`;
return (
<form className={classes.form} onSubmit={confirmHandler}>
{/* order ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ํ๋๊ฒ ํจ */}
<div className={`${classes.control} ${formInputsValidity.name? "" : classes.invalid}`}>
<label htmlFor="name">์ด๋ฆ</label>
<input type="text" id="name" ref={nameInputRef} />
{/* ์ด๋ฆ์ ์ ์ ์์ ๋ ๋ํ๋๊ฒ. formInputsValidity๊ฐ false์ผ ๋ */}
{!formInputsValidity.name && <p>์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์</p>}
</div>
<div className={addressControlClass}>
<label htmlFor="address">์ฃผ์</label>
<input type="text" id="address" ref={addressInputRef} />
{!formInputsValidity.name && <p>์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์</p>}
</div>
<div className={telControlClass}>
<label htmlFor="tel">ํธ๋ํฐ ๋ฒํธ(์ซ์๋ง 11์๋ฆฌ ์
๋ ฅํ์ธ์)</label>
<input type="text" id="tel" ref={telInputRef} />
{!formInputsValidity.name && <p>ํธ๋ํฐ ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์(์ซ์๋ง 11์๋ฆฌ)</p>}
</div>
<div className={classes.action}>
<button className={classes["button-outline"]} onClick={props.onClick}>Cancel</button>
{/* ์
๋ ฅ ์ ๋ฐฑ์๋์ ๋ฐ์ดํฐ ๋๊ฒจ์ฃผ๋๋ก ํ๋ ๋ฒํผ. form์ onSubmit์์ ์๋ */}
<button className={classes.button}>Confirm</button>
</div>
</form>
);
};
export default Checkout;
์ด๋ฏธ์ง๋ก ๋์ฒด
firebase์ ์ถ๊ฐ๋จ
npm
- ์ค์น
$ npm install react-router-dom localforage match-sorter sort-by- ์คํ
$ npm run dev
yarn
- ์ค์น
$ yarn add react-router-dom localforage match-sorter sort-by- ์คํ
$ yarn start
App.js
import './App.css';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from './pages/Home';
import NotFound from './pages/NotFound';
import Work from './pages/Work';
import Profile from './pages/Profile';
const routers = createBrowserRouter([
{
path: "/", // ์ฃผ์์ฐฝ์ ๋ค์ด๊ฐ๋ ํ
์คํธ. ("/" - ๊ฐ์ฅ ์์ ๋ฌธ์๋ฅผ ๋ํ๋. index.html)
element: <Home />,
errorElement: <NotFound />
},
{
path: "/works",
element: <Work />,
},
{
path: "/profile",
element: <Profile />,
},
])
function App() {
return (
<>
<RouterProvider router={routers} />
</>
);
}
export default App;
Home.jsx
import React from 'react'
const Home = () => {
return (
<div>Home</div>
)
}
export default Home
Work.jsx
import React from 'react'
const Work = () => {
return (
<div>๋ด ์์
๋ฌผ๋ค</div>
)
}
export default Work
NotFound.jsx
import React from 'react'
const NotFound = () => {
return (
<div>
<p>Not Foundโ</p>
</div>
)
}
export default NotFound
Profile.jsx
import React from 'react'
const Profile = () => {
return (
<div>
<br /><br /><br /><br />
ํ๋กํ์
๋๋ค.
</div>
)
}
export default Profile