일마다 나눠쓰기 힘들어서 한 번에 작성
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 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 mealsList = DUMMY_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;
먼저 회원가입을 한 후 github와 연결한다.
(작업중)