Order

김종민·2022년 10월 6일
0

Nuber-Client

목록 보기
15/21

들어가기
Client가 Restaurant에서 menu를 보고
주문을 하는 page.
Restaurant Detail Page,
Dish Component,
dish-option Component로 구성됨.
menu를 Click했을떄, menu가 Item에 담기는 과정
option을 Click했을떄, options이 Item에 담기는 과정이
매우 어렵고 난해하므로 집중할것!!!


1. src/pages/client/restaurantDetail.tsx

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

2. src/components/dish.tsx

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)임.
  

3. src/components/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부분은 너무너무 복잡해서 보고또보고또보고 해야할듯.
!!! 이부분을 잘 익히면, 쇼핑몰같은 전자상거래 웹서비스를 만드는게
!!! 쉬워질듯

profile
코딩하는초딩쌤

0개의 댓글