Product Detail, Related Product, useSWR, mutate

김종민·2022년 8월 5일
0

apple-market

목록 보기
19/37


index.tsx(Home)에서
상품 하나를 클릭했을때,
상품의 Detail page 및 similar Product를 불러올수 있게 한다

1. pages/products/[id].tsx

import useMutation from '@libs/client/useMutation'
import useUser from '@libs/client/useUser'
import { cls } from '@libs/client/utils'
import { Product, User } from '@prisma/client'
import type { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { userAgent } from 'next/server'
import useSWR, { useSWRConfig } from 'swr'
import Button from '../../components/button'
import Layout from '../../components/layout'

interface ProductWithUser extends Product {
  user: User
}
///Product(prisma) type에 user를 추가한 ProductWithUser interface를 만듬.

interface ItemDetailResponse {
  ok: boolean
  product: ProductWithUser  ///ItemDetailResponse에서 Product에 user 추가한
                            ///type을 만듬.
  relatedProducts: Product[]  ///similar item의 type은 Product[]임.
  isLiked: boolean            ///나중에 loggedInUser가 좋아요 누를지를 알아보는 prop.
}

const ItemDetail: NextPage = () => {
  const { user, isLoading } = useUser() ///useUser hook을 이용해서 loggedInUser확인
  const router = useRouter()
  // console.log(router.query) -->product id를 따냄.
  
  const { mutate } = useSWRConfig() 
  ///unBoundMutate가 사용된 예시는 useUser.tsx에서 확임
  ///여기의 mutate는 unBoundMutate로 현재 page(ItemDetail)가 아닌,
  ///다른 파일 혹은 page의 cache를 다시 write하는 mutate
  ///밑의 mutate:boundMutate는 현재 page의 하트를 눌렀을떄,
  ///바로 색을 빨간색으로 바꿔줌.
  
  const { data, mutate: boundMutate } = useSWR<ItemDetailResponse>(
    router.query.id ? `/api/products/${router.query.id}` : null
  )
  ///router.query.id가 있으면 요청을 하는 로직으로 해야함. 안그럼 undefined나옴
  ///useSWR을 이용해서 `/api/products/${router,query.id}' API에서
  ///product의 data를 받아옴,
  ///router.query.id는 index.tsx 페이지에서 productId가 1인 상품을
  ///클릭하면, router.query.id 는 1이 나옴.
  ///위의 const router=useRouter()로 router.query.id 받음.
  ///mutate:boundMutate는 server를 거치지않고 cache를 rewrite함.
  ///graphql의 cache Write를 생각하면됨, 하트를 누르면, 바로 빨간색으로 변하게~

  const [toggleFav] = useMutation(`/api/products/${router.query.id}/fav`)
  ///heart를 눌렀을떄, `/api/products/${router.query.id}/fav` API주소로
  ///req 날림, 위 API 처리 로직은 아래에서 다룸. heart를 눌러서 좋아요 +1, 색을 빨간색
  ///으로 바꾸는 API
  ///useMutation으로 사용하는것을 알아둔다

  const onFavClick = () => {
    if (!data) return
    toggleFav({})
    boundMutate({ ...data, isLiked: !data.isLiked }, false)
    //mutate('/api/users/me', { ok: false }, false)
    //mutate('/api/users/me', (prev:any)=>({ok:!prev.ok}), false), 함수형도가능
    //mutate('/api/users/me') , 단순히 refetch도 가능함.
    ===>unBoundMutate가 사용된 예시, /api/user/me 의 ok data를 false로 다시씀.
  }
  ///heart를 눟헜을때, toggleFav useMutation을 실행시키고, 
  ///boundMutate로 하트를 빨간색으로 바꿈,
  ///두번쩨, false를 DB 를 다시 reload하지 않겠다는 뜻,
  ///true로 하면 앞의 argument실행 후, DB다시 reload함.
  
  return (
    <Layout canGoBack>
      <div className="px-4 py-10">
        <div>
          <div className="h-96 bg-slate-400" />
          <div className="flex cursor-pointer py-3 border-b items-center space-x-3">
            <div className="w-12 h-12 rounded-full bg-fuchsia-300" />
            <div>
              <p className="text-sm font-medium text-gray-700">
                {data?.product?.user?.name}
              </p>
              <Link href={`/users/profiles/${data?.product?.user?.id}`}>
              ///user의 profile page로 이동시킴~
                <a className="rext-xs font-medium">View profile &rarr;</a>
              </Link>
            </div>
          </div>
          <div className="mt-10 ">
            <h1 className="text-3xl font-bold text-gray-900 ">
              {data?.product?.name}
            </h1>
            <p className="text-3xl mt-3 pb-5 text-slate-400 font-black border-b-2">
              {data?.product?.price}원
            </p>
            <p className="text-base my-6 text-gray-800">
              {data?.product?.description}
            </p>
            <div className="flex items-center justify-between">
              <Button large text="Talk to seller" />
              <button
                onClick={onFavClick} ///위에서 만든 heart를 눌렀을떄,
                                     ///onFavClick함수가 실행되게!
                className={cls(
                  'p-3 mx-3 rounde flex items-center justify-center',
                  data?.isLiked
                    ? 'text-red-500 hover:text-red-300'
                    : 'text-gray-400  hover:text-gray-200'
                )}
              >
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  className="h-9 w-9"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path
                    fillRule="evenodd"
                    d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
                    clipRule="evenodd"
                  />
                </svg>
              </button>
            </div>
          </div>
        </div>
        <div className="mt-6">
          <h2 className="text-2xl font-bold text-gray-800">Similar items</h2>
          <div className="mt-5 grid grid-cols-3 gap-4">
            {data?.relatedProducts.map((product) => (
            ///similar Item을 불러줌!!
            
              <div key={product.id}>
                <div className="h-36 w-full bg-slate-400" />
                <h3>{product.name}</h3>
                <p className="text-sm font-medium text-gray-900">
                  {product.price}원
                </p>
              </div>
            ))}
          </div>
        </div>
      </div>
    </Layout>
  )
}

export default ItemDetail

2. pages/products/[id]/index.ts

상품 하나하나의 Detail을 요청하는 API

import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import client from '@libs/server/client'
import { withApiSession } from '@libs/server/withSession'

async function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseType>
) {
  const { id } = req.query ///클릭한 상품의 id를 req.query로 받음.
  const { user } = req.session ///loggedInUser를 req.session으로 받음.
  //console.log(id) product id를 받아옴.
  //   const product = await client.product.findUnique({
  //     where: {},
  //   })
  const product = await client.product.findUnique({
    where: {
      id: Number(id), ///id를 Int로 만들어줌.
    },
    include: {
      user: {
        select: {
          id: true,
          name: true,
          avatar: true,  ///req.query로 받은 상품 id로 그 상품을 찾음.
                         ///include로 그 상품을 올린 user의 id, name, avatar를
                         ///감티 받아옴
        },
      },
    },
  })
  
  const terms = product?.name.split(' ').map((word) => ({
    name: {
      contains: word,
    },
  }))
  ///위에서 찾은 product의 name을 받아서 map을 이용해서 
  ///배열로 만들어 줌
  ///ex) samsung galaxy s60 => [samsung, galaxy, s60]으로~
  
  const relatedProducts = await client.product.findMany({
    where: {
      OR: terms,
      AND: {
        id: {
          not: Number(id), ///id는 Number이어야 하므로, Number(id)로 해줌.
        },
      },
    },
  })
  ///위에서 만든 terms(product.name을 분리시켜서 배열로 만들어줌)
  ///분리된 word들이 들어간 상품을 찾음
  ///단, 위에서 받은 req.query로 받은 상품은 포함시키지 않음
  
  console.log(relatedProducts)
  
  const isLiked = Boolean(
    await client.fav.findFirst({ ///fav model에서 찾음. productId, userId
                                  ///두개다 해주어야 함.
                                  ///두개라서 findFirst를 사용
                                  ///select는 모든 정보불러오면, server힘듬.
      where: {
        productId: product?.id,
        userId: user?.id,
      },
      select: {
        id: true,
      },
    })
  )
  ///req.query로 받은 아이디의 product에 loggedInUser가 heart를 누른지, 안누른지를
  ///알려주는 로직, Boolean으로 감싸준것을 check!!! true, false로 return됨.
  
  res.json({ ok: true, product, isLiked, relatedProducts })
  ///하나의 product에 관련된, product, isLoked, relatedProducts를
  ///return해줌.
}

export default withApiSession(
  withHandler({
    methods: ['GET'],
    handler,
  })
)

3. pages/api/[id]/fav.ts

///heart를 눌렀을때, 처리되는 API
///헷갈릴 경우 위위 POST의 fav model(schema.prisma)를 참고할 것!!!

import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import client from '@libs/server/client'
import { withApiSession } from '@libs/server/withSession'

async function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseType>
) {
  const { id } = req.query  ///heart를 누른 상품의 id를 받아옴.
  const { user } = req.session  ///loggedInUser확인, 
  const alreadyExists = await client.fav.findFirst({
  ///내가 이 상품의 heart를 눌렀는지 안눌렀는지를  먼저 확인함.
  ///fav DB에서 productId와 userId에서~
  
    where: {
      productId: Number(id),
      userId: user?.id,
    },
  })
 
  if (alreadyExists) {
    await client.fav.delete({
      where: {
        id: alreadyExists.id,
      },
    })
    ///내가 heart를 누른 상태라면, fav를 delete시킴(관심목록에서 삭제)
    
  } else {
  ///내가 heart를 누른 상태가 아니라면 fav를 만듬.
  ///user랑 product를 connect해주면, 만들어짐.
  //헷갈리면 prisma.schema fav DB를 참고할 것!!
    await client.fav.create({
      data: {
        user: {
          connect: {
            id: user?.id,
          },
        },
        product: {
          connect: {
            id: Number(id),
          },
        },
      },
    })
   
  }
  res.json({ ok: true })
}

export default withApiSession(
  withHandler({
    methods: ['POST'],
    handler,
  })
)

NOTICE!!!
이번장에서는 상품의 detail Page에 대해서 알아봄.
1. heart를 눌러서 관심 목록에 넣는거 확인
2. heart를 눌러서 하트의 색깔을 바로 바꾸는것
3. useSWR의 boubdMutate와 unBoundMutate의 차이점, 쓰임을 알아놓을것
4. req.query, req,session, router.query 등등 확실히 인지할것

profile
코딩하는초딩쌤

0개의 댓글