App.js
import React, { useCallback, useEffect, useState } from "react";
import "./App.scss";
import MovieList from "./components/MovieList";
import AddMovie from "./components/AddMovie";
function App() {
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null); // ์๋ฌ ์๋ ์ํ
/*
// ๋น๋๊ธฐโญ
function fetchMovieHandler() {
fetch("https://swapi.dev/api/films/")
.then((response) => {
return response.json(); // json ๋์ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ ์ ์๋ ์๋ฐ์คํฌ๋ฆฝํธ ํํ๋ก ๋ณํ
})
.then((data) => {
const transformedMovies = data.results.map((movieData) => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
// ํ์ํ 4๊ฐ๋ง ์๋ก์ด ๊ฐ์ฒด๋ก ๋ฐํ
};
});
setMovies(transformedMovies);
});
}
*/
// ๋น๋๊ธฐโ (async, await)
/*
async function fetchMovieHandler() {
setIsLoading(true);
setError(null);
try {
const response = await fetch("https://swapi.dev/api/films/");
if (!response.ok) {
throw new Error("์๋ฌ ๋ฐ์"); // ๋ท๋ถ๋ถ ์ฝ๋ ์งํx
}
const data = await response.json();
const transformedMovies = data.results.map((movieData) => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
// ํ์ํ 4๊ฐ๋ง ์๋ก์ด ๊ฐ์ฒด๋ก ๋ฐํ
};
});
setMovies(transformedMovies);
} catch(error) {
setError(error.message);
}
setIsLoading(false);
}
*/
// const ํ์์ผ๋ก ๋ณ๊ฒฝ
const fetchMovieHandler = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://movietest-34408-default-rtdb.asia-southeast1.firebasedatabase.app/movie.json');
if (!response.ok) {
throw new Error("์๋ฌ ๋ฐ์"); // ๋ท๋ถ๋ถ ์ฝ๋ ์งํx
}
const data = await response.json();
// console.log("๋ฐ์์จ data๋? : ", data);
// firebase์์ ๋ฐ์์จ ํ์ผ์ ๋ณํ์ํจ ๊ฐ์ฒด
const loadMovies = []; // ๋ฐ์์จ ๊ฐ์ฒด๋ฅผ ๋ฐฐ์ด๋ก ์ ์ธ
for (const key in data) {
// ๋ฐฐ์ด ์์ ์ง์ด๋ฃ์
loadMovies.push({
id: key,
title: data[key].title,
openingText: data[key].openingText,
releaseDate: data[key].releaseDate,
})
}
// console.log("loadMovies๋? : ", loadMovies);
setMovies(loadMovies);
// ์์ for๋ฌธ์ผ๋ก ๋์ฒด
// const transformedMovies = data.results.map((movieData) => {
// return {
// id: movieData.episode_id,
// title: movieData.title,
// openingText: movieData.opening_crawl,
// releaseDate: movieData.release_date,
// // ํ์ํ 4๊ฐ๋ง ์๋ก์ด ๊ฐ์ฒด๋ก ๋ฐํ
// };
// });
// setMovies(transformedMovies);
} catch (error) {
setError(error.message);
}
setIsLoading(false);
}, []); // [] - ํ๋ฉด ์ด๋ฆด ๋ ์ฒ์์๋ง ์๋๋จ
// fetchMovieHandler ํจ์๊ฐ ํธ์ถ๋ ๋๋ ๋ฐ์.
// ํจ์๋ ์ธ๋ถ์์ ๋ฐ๋ ์ ์์(ex. ๋ณ์ ๋ฐ์์ด) -> ๋ฌดํ๋ฃจํ ๊ฐ๋ฅ์ฑ ์์
useEffect(() => {
fetchMovieHandler();
}, [fetchMovieHandler]);
// useCallback(()=>{}, [])
// ์ถ๊ฐํ๋ ํจ์
const addMovieHandler = (item) => {
// console.log(item);
fetch(
"https://movietest-34408-default-rtdb.asia-southeast1.firebasedatabase.app/movie.json",
{
method: "POST", // firebase์ ๋ฆฌ์์ค ๋ง๋ฆ
body: JSON.stringify(item), // js ๊ฐ์ฒด๋ฅผ json์ผ๋ก ๋ณํ
// headers: { "Content-Type": "application/json" }, // firebase์์๋ ์๋ต ๊ฐ๋ฅ, API์์ ์ด๋ค ์ปจํ
์ธ ๊ฐ ์ ๋ฌ๋๋์ง ์ ์ ์๊ฒ ํด์ค
}
);
};
let content = <p>NO MOVIES</p>;
if (isLoading) {
content = <p>LOADING...</p>;
}
if (movies.length !== 0) {
content = <MovieList movie={movies} />;
}
if (error) {
content = <p>{error}</p>;
}
return (
<main>
<section>
<AddMovie onAddMovie={addMovieHandler} />
</section>
<section>
<button onClick={fetchMovieHandler}>Fetch Movie</button>
</section>
<section>
{content}
{/* isLoading์ด false์ผ ๋
{!isLoading && <MovieList movie={movies} />}
{!isLoading && movies.length === 0 && <p>NO MOVIES</p>}
{isLoading && <p>LOADING...</p>} */}
</section>
</main>
);
}
export default App;
MovieList.jsx
import React from "react";
import Movie from "./Movie";
const MovieList = (props) => {
return (
// map()์ ์ฌ์ฉํ์ฌ ํ๋ฉด์ ์ถ๋ ฅ
<ul>
{props.movie.map((item) => (
<Movie
key={item.id}
id={item.id}
title={item.title}
releaseDate={item.releaseDate}
openingText={item.openingText}
/>
))}
</ul>
);
};
export default MovieList;
Movie.jsx
import React from "react";
const Movie = (props) => {
return (
<li className="movie">
<h2>{props.title}</h2>
<h3>{props.releaseDate}</h3>
<p>{props.openingText}</p>
</li>
);
};
export default Movie;
AddMovie.jsx
import React, { useRef } from "react";
const AddMovie = (props) => {
const titleRef = useRef("");
const textRef = useRef("");
const dataRef = useRef("");
const submitHandler = (e) => {
e.preventDefault();
const movie = {
title: titleRef.current.value,
openingText: textRef.current.value,
releaseDate: dataRef.current.value,
};
props.onAddMovie(movie);
clearValue();
};
// ์
๋ ฅ ํ input ์ฐฝ ์ด๊ธฐํ
const clearValue = () => {
titleRef.current.value = "";
textRef.current.value = "";
dataRef.current.value = "";
}
return (
<form onSubmit={submitHandler}>
<div className="control">
<label htmlFor="title">title</label>
<input type="text" id="title" ref={titleRef} />
</div>
<div className="control">
<label htmlFor="text">text</label>
<textarea id="text" rows="5" ref={textRef}></textarea>
</div>
<div className="control">
<label htmlFor="date">date</label>
<input type="text" id="date" ref={dataRef} />
</div>
<button>Add Movie</button>
</form>
);
};
export default AddMovie;
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 } from "react";
import Modal from "../UI/Modal";
import classes from "./Cart.module.css";
import CartContext from "../../store/cart-context";
const Cart = (props) => {
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});
};
// ๊ณ์ ๋ณํํ๋ ๊ฐ
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>
);
return (
// ๋ชจ๋ฌ ์์ ๋ชจ๋ ๋ด์ฉ์ ์ง์ด๋ฃ์
<Modal onClose={props.onClose}>
<div>
{cartItem}
<div className={classes.total}>
<span>Total Amount</span>
<span>{totalAmount}</span>
</div>
<div className={classes.action}>
<button className={classes["button-outline"]} onClick={props.onClose}>
Close
</button>
{/* hasItems๊ฐ true์ผ ๋๋ง Order๋ฒํผ์ด ๋ณด์ด๊ฒ */}
{hasItems && <button className={classes.button}>Order</button>}
</div>
</div>
</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) => {},
});
// ์ฅ๋ฐ๊ตฌ๋ ํญ๋ชฉ ์ด๊ธฐ๊ฐ
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, // ?
// }
// }
// 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 });
};
// 1
// ์
๋ฐ์ดํธ ๋ ๊ฐ์ฒด - ๋ค์ด๋๋ฏนํ๊ฒ ๋ณํ๋ ๋ถ๋ถ(๊ฒ์ ๋ณํ๋ ๋ถ๋ถ)
// items, totalAmount ๋ ๊ฐ๋ฅผ ์ ์ฅํ๋ ๊ฒ์ด ๋ชฉ์ ์. ๋๋จธ์ง๋ ํจ์ ์ ์
const cartContext = {
items: cartState.items, // ์์ดํ
์ด ๋ค์ด์๋ ๋ฐฐ์ด
totalAmount: cartState.totalAmount, // ์ด ๊ฐฏ์
addItem: addItemToCartHandler,
removeItem: removeItemFromCartHandler,
};
return (
// 1-1
<CartContext.Provider value={cartContext}>
{props.children}
</CartContext.Provider>
);
};
export default CartProvider;