들어가기
Client가 Restaurant에서 menu를 보고
주문을 하는 page.
Restaurant Detail Page,
Dish Component,
dish-option Component로 구성됨.
menu를 Click했을떄, menu가 Item에 담기는 과정
option을 Click했을떄, options이 Item에 담기는 과정이
매우 어렵고 난해하므로 집중할것!!!
import { gql, useMutation, useQuery } from '@apollo/client'
import React, { useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Dish } from '../../components/dish'
import { DishOptionFront } from '../../components/dish-option'
import { DISH_FRAGMENT, RESTAURANT_FRAGMENT } from '../../fragments'
import {
CreateOrderItemInput,
CreateOrderMutation,
CreateOrderMutationVariables,
RestaurantQuery,
RestaurantQueryVariables,
} from '../../graphql/__generated__'
const RESTAURANT_QUERY = gql`
query restaurant($input: RestaurantInput!) {
restaurant(input: $input) {
ok
error
restaurant {
...RestaurantParts
menu {
...DishParts
}
}
}
}
${RESTAURANT_FRAGMENT}
${DISH_FRAGMENT}
`
///주문을 하기 위해서 restaurants에서 식당을 click했을떄,
///그 식당의 detail page로 이동함.
///여기서 주문을 때림.
const CREATE_ORDER_MUTATION = gql`
mutation createOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
ok
error
orderId
}
}
`
///server에서 createOrder mutation을 불러옴.
type IRestaurantParams = {
id: string
}
///restaurant Id를 path에서 받아내기 위한 Params
const RestaurantDetail = () => {
const { id } = useParams() as unknown as IRestaurantParams
console.log(id)
const { loading, data } = useQuery<RestaurantQuery, RestaurantQueryVariables>(
RESTAURANT_QUERY,
{
variables: {
input: {
restaurantId: Number(id),
},
},
}
)
///path에서 id를 받아와서 그 id로 restaurant의 data를
///불러옴.
const [orderStarted, setOrderStarted] = useState(false)
///Start Order버튼을 클릭하고 나서 주문을 할 수 있게 설정함.
const [orderItems, setOrderItems] = useState<CreateOrderItemInput[]>([])
///주문하기 위해서 클릭한 menu들을 담기 위해서 orerItems를
///useState 배열로 만듬.
///type이 CreateOrderItemInput[] 배열임을 명심할 것!!
//주문 시작
const triggerStartOrder = () => {
setOrderStarted((current) => !current)
}
///주문시작버튼에 들어갈 함수, orderStarted가 true로 바뀜.
//선택한 주문 내역return
const getItem = (dishId: number) => {
return orderItems.find((order) => order.dishId === dishId)
}
///dishId를 입력받아서, 위의 useState인 orderItems에
///입력받은 dish가 있는지 확인해서 그 dish를 return해줌.
//선택했는지 안했는지 return->true/false
const isSelected = (dishId: number) => {
return Boolean(getItem(dishId))
}
///dishId를 입력받아서, 위의 getItem함수를 이용해서,
///getItem에 dishId를 입력받아, 그 dish가
///orderItems에 포함되었는지 여부에 따라 true/false를 return
//주문내역 추가
const addItemToOrder = (dishId: number) => {
if (isSelected(dishId)) {
return
}
setOrderItems((current) => [{ dishId, options: [] }, ...current])
}
///menu를 클릭했을떄, 그 menu(dish)가 orderItems에 담기게함.
///이미 selected되었으며, choice못되게 바로 return시킴.
///setOrderItems에 담기는 문법을 잘 봐둘것.
//주문내역삭제
const removeFromOrder = (dishId: number) => {
setOrderItems((current) => current.filter((dish) => dish.dishId !== dishId))
}
///Click하면 menu가 orderItems에서 삭제되는 문법.
///filter를 통해서 삭제되는 과정을 잘 봐둘것.
//선택한 주문내역에 option선택시 추가
const addOptionsToItem = (dishId: number, optionName: string) => {
if (!isSelected(dishId)) {
return
}
///dish의 option을 추가 하기위한 로직.
///dish가 selected되어야 option도
///choice될 수 있게 함.
const oldItem = getItem(dishId)
///getItem함수를 이용해 옵션을 추가하려는 dish를 불러와서
///oldItem에 담는다.
if (oldItem) {
const hasOption = Boolean(
oldItem.options?.find((aOption) => aOption.name === optionName)
)
///oldItem이 있을떄,
///oldItem의 option이 있는지 확인한다.
///oldItem의 옵션name과 클릭해서 입력받는 optionName을
///찾아서 Boolean으로 return시킴.
if (!hasOption) {
removeFromOrder(dishId)
setOrderItems((current) => [
{ dishId, options: [{ name: optionName }, ...oldItem.options!] },
...current,
])
}
///oldItem에 option이 없으면, 기존의 dishId만 담긴
///dish를 orderItems에서 remove 한 다음,
///orderItems에 dishId와 option을 같이 넣어준다.
///dishId와 option을 같이 넣어주는 로직이 상당히
///까탈스러우니 집중해서 봐둘것!!!
}
}
//선택한 주문에서 option return
const getOptionFromItem = (
item: CreateOrderItemInput, ///item의 type임
optionName: string
) => {
return item.options?.find((option) => option.name === optionName)
}
///item과 optionName을 입력받아서, item의 option안의 name과
///입력받은 optionName이 같은것을 return해 준다.
//option select 여부
const isOptionSelected = (dishId: number, optionName: string) => {
const item = getItem(dishId)
if (item) {
return Boolean(getOptionFromItem(item, optionName))
}
return false
}
///dishId와 optionName을 입력받아서,
///dishId로 orderItems안에 들어있는 dish를 찾은 다음,
///입력받은 optionName과 입력받은 dish의 option에 name이
///있는지를 확인해서 Boolean으로 return해 준다.
///item이 없을 경우를 대비해서, return false를 꼭 써줘야한다.
//select한 option 삭제
const removeOptionFromItem = (dishId: number, optionName: string) => {
if (!isSelected(dishId)) {
return
}
///우선, selected된 dish가 없으면, return한다.
const oldItem = getItem(dishId)
///입력받은 dishId로 orderItems속에서 dish(item)을 찾는다.
if (oldItem) {
setOrderItems((current) => [
{
dishId,
options: oldItem.options?.filter(
(option) => option.name !== optionName
),
},
...current,
])
}
///dishId와 optionName을 입력받아, 입력받은 optionName과
///입력받은 dish(dishId)의 option.name과 다른것만
///return한다.
if (!oldItem) {
return
}
}
const triggerCancelOrder = () => {
setOrderStarted(false)
setOrderItems([])
}
///Order 취소버튼 함수.
///setOrderStarted를 false로 바꾸고,
///setOrderItems를 빈배열로 바꿔준다.
const navigate = useNavigate()
///order confirm 후, order page로 redirect되게 할려고~
const onCompleted = (data: CreateOrderMutation) => {
console.log(data)
if (data.createOrder.ok) {
const {
createOrder: { ok, orderId },
} = data
navigate(`/orders/${orderId}`)
}
}
///createOrderMutation실행 후, return으로 orderId를
///받아서, `/orders/${orderId}` 페이지로 이동함.
const [createOrderMutation, { loading: placingOrder }] = useMutation<
CreateOrderMutation,
CreateOrderMutationVariables
>(CREATE_ORDER_MUTATION, {
onCompleted,
})
///createOrderMutation임.~
//보안 및 다중클릭방지, 주문확인버튼 함수.
const triggerConfirmOrder = () => {
if (placingOrder) {
return
}
///loading중이면, return함.
if (orderItems.length === 0) {
alert('Can not place empty order')
return
}
///주문할 물건이 하나도 없는경우.
const ok = window.confirm('You are about to place an order')
///팝업으로 확인버튼 누르게 띄우는것!
if (ok) {
createOrderMutation({
variables: {
input: {
restaurantId: +id,
items: orderItems,
},
},
})
}
///팝업으로 뜬 것을 확인버튼을 누르면, createOrderMutation이
///실행되게 함, restaurantId와 items를
///variables로 입력해줌.
}
return (
<div>
<div
className=" py-40 bg-cover"
style={{
backgroundImage: `url(${data?.restaurant.restaurant?.coverImg})`,
}}
>
///image넣는 문법, style={{}} 확인할 것,
///option은 bg-cover, bg-center 등등..
<div className="bg-white w-1/2 py-8 opacity-60 ">
<h4 className="text-4xl mb-3">{data?.restaurant.restaurant?.name}</h4>
<h5 className="test-sm font-light mb-2">
{data?.restaurant.restaurant?.category?.name}
</h5>
<h6 className="test-sm font-light">
{data?.restaurant.restaurant?.address}
</h6>
</div>
///backbroundImage안에다가 text넣기~~
</div>
<div className="ml-5 mr-5 container pb-32 flex flex-col items-end mt-20">
{!orderStarted && (
<button onClick={triggerStartOrder} className="btn px-5 rounded-lg">
Start Order
</button>
)}
///Start Order 버튼을 누르면, orderStarted가
///true로 변하게 하는 button.
{orderStarted && (
<div>
<button
className="btn px-10 mr-3 rounded-lg"
onClick={triggerConfirmOrder}
>
Confirm Order
</button>
<button
onClick={triggerCancelOrder}
className="btn px-10 rounded-lg"
>
Cancel Order
</button>
</div>
)}
///orderStarted가 true이면, Confirm Order과
///Cancel 버튼이 보이게 하는 로직.
///여기서부터 메뉴를 뿌려줌.
<div className="w-full grid mt-16 md:grid-cols-3 pag-x-5 gap-x-5 gap-y-10">
{data?.restaurant.restaurant?.menu.map((dish: any, index: any) => (
///menu를 map으로 뿌려줌,
///위의 많은 함수들에서 ()안에 받은 dishId는
///dish.id로 보내줌.
<Dish
isSelected={isSelected(dish.id)}
id={dish.id}
orderStarted={orderStarted}
key={index}
name={dish.name}
description={dish.description}
price={dish.price}
isCustomer={true}
options={dish.options}
addItemToOrder={addItemToOrder}
removeFromOrder={removeFromOrder}
>
///Dish component에 보내주는 props들.
///Dish component안에 DishOptionFront component
///를 넣어서 매뉴를 만들어줌.
{dish.options?.map((option: any, index: any) => (
///dish.options를 map으로 뿌려줌.
<DishOptionFront
key={index}
isSelected={isOptionSelected(dish.id, option.name)}
///위의 isOptionSelected 함수가 받는
///dishId와 optionName은
///dish.id와 option.name으로 보내줌.
name={option.name}
extra={option.extra}
dishId={dish.id}
addOptionsToItem={addOptionsToItem}
removeOptionFromItem={removeOptionFromItem}
/>
))}
</Dish>
))}
</div>
</div>
</div>
)
}
export default RestaurantDetail
Dish Component
import React from 'react'
import { DishOption } from '../graphql/__generated__'
interface IDishProps {
id?: number
description: string
name: string
price: number
isCustomer?: boolean
orderStarted?: boolean
options?: DishOption[] | null
///options의 type은 DishOption[]으로 해줌.
addItemToOrder?: (dishId: number) => void ///함수타입.
removeFromOrder?: (dishId: number) => void ///함수타입.
isSelected?: boolean
children?: React.ReactNode
///Dish component안에 dish-option component를
///받기 때문에 children?:React.ReactNode로 type설정해줌.
}
export const Dish: React.FC<IDishProps> = ({
id = 0, ///id의 기본값은 0으로, id={dish.id}으로 보냐줬음
///restaurant Detail Page에서.
description,
name,
price,
isCustomer = false,
orderStarted = false,
options,
addItemToOrder,
removeFromOrder,
isSelected,
children: dishOptions,
///Dish component안에 넣어준 dish-option은
///children:dishOptions로 설정해줌.
}) => {
const onClick = () => {
if (orderStarted) {
if (!isSelected && addItemToOrder) {
return addItemToOrder(id)
}
}
///id는 restaurantDetailPage에서 dish.id로
///id를 보내줬음.
///isSelected가 되어있지않고, addItemToOrder이 있으면,
///클릭시 orderItems에 add가 되게하는 함수이자 버튼.
///addItemToOrder에 id가 들어가는것을 집중해서 볼것!!
///이 id가 위위의 addToOrder에 보내지는 dishId임.
if (isSelected && removeFromOrder) {
return removeFromOrder(id)
}
///id는 restaurantDetailPage에서 dish.id로
///id를 보내줬음.
///isSelected가 되어있지, removeFromOrder가 있으면,
///클릭시 orderItems에서 remove가 되게하는 함수이자 버튼.
///addItemToOrder에 id가 들어가는것을 집중해서 볼것!!
///이 id가 위위의 addToOrder에 보내지는 dishId임.
}
return (
<div
className={`px-8 py-4 border cursor-pointer transition-all ${
isSelected
? 'border-gray-600'
: 'hover:border-gray-900 hover:shadow-xl '
}`}
///className에서 변수가 있을시, 작성되는
///문법을 집중해서 볼것!!
>
<div className="mb-5">
<h3 className="text-lg font-medium flex items-center">
{name}{' '}
///Order Start버튼을 눌렀을 시,
///orderStarted가 true로 변했을 시,~
{orderStarted && (
<button
className={`ml-3 py-1 px-3 focus:outline-none text-sm text-white ${
isSelected ? 'bg-red-500' : 'bg-green-400'
}`}
onClick={onClick}
>
{isSelected ? 'Remove' : 'Add'}
</button>
)}
///dish를 add 혹은 remove하는 button.
</h3>
<h4>{description}</h4>
</div>
<span>${price}</span>
///UserRole이 customer일때에만, dishOption이 보여지게
///코딩함.
{isCustomer && options && options.length !== 0 && (
<div>
<h5 className="mt-8 font-medium">Dish Options:</h5>
<div className="grid gap-2 justify-start">{dishOptions}</div>
</div>
)}
</div>
)
}
///{dishOptions}는 restaurant Detail page에서 들어가
///component인 DishOptionFron(dish-option.tsx)임.
!!!restaurant Detail Page에 들어간
DishOptionFront(dish-option.tsx)임.
import React from 'react'
interface IDishOptionProps {
isSelected: boolean
name: string
extra?: number | null
dishId: number
addOptionsToItem: (dishId: number, options: any) => void
removeOptionFromItem: (dishId: number, optionsName: string) => void
}
///restaurant Detail page에서 받는 props들
///addOptionsToItem이랑 removeOptionFromItem에서 받는
///dishId와 name은 dish.id와 option.name으로 보내짐.
export const DishOptionFront: React.FC<IDishOptionProps> = ({
isSelected,
name,
extra,
dishId,
addOptionsToItem,
removeOptionFromItem,
}) => {
const onClick = () => {
if (isSelected) {
removeOptionFromItem(dishId, name)
///DetailPage에서 보내준 dishId, name을 변수로 넣음.
} else {
addOptionsToItem(dishId, name)
///DetailPage에서 보내준 dishId, name을 변수로 넣음.
}
}
return (
<div
onClick={onClick}
className={`flex border items-center ${
isSelected ? 'bg-red-400 rounded-lg' : 'hover:bg-red-100'
}`}
///isSelected여부에 따라 다르게 design되는
///tailwindCss 코딩임. 집중해서 봐둘것!!!
>
<span className="mr-2 px-5 ">{name}</span>
{<span className="tsxt-sm oprcity-70 px-10">(${extra})</span>}
</div>
)
}
!!!Order부분은 너무너무 복잡해서 보고또보고또보고 해야할듯.
!!! 이부분을 잘 익히면, 쇼핑몰같은 전자상거래 웹서비스를 만드는게
!!! 쉬워질듯