Next.js + Supabase 장바구니 구현(1) - 담기편

이지·2024년 9월 6일
1

Project

목록 보기
5/9
post-thumbnail

Supabase 장바구니 관련 Table 설정

유저의 ID 당 하나의 cart를 갖도록 하고 cart_item 테이블을 통해 각각의 상품이 담기도록 구성했다.

cart table 생성

cart_item table 생성

각 테이블의 관계도 확인

cart와 cart_item의 Schema Visualizer(ERD)는 다음과 같다.

Schema Visualizer 확인하는 방법
Database>Tools>Schema Visualizer

상품 상세 페이지

장바구니에 상품을 담기위해 다음과 같은 상황을 판단해야 한다.

1. 로그인 상태 확인
   1.1 로그인하지 않음
       - 로그인 페이지 유도
   1.2 로그인함
       - 로그인한 사용자의 역할을 확인

2. 판매자로 로그인함
   - 로그인 페이지 유도
   
3. 구매자로 로그인함
   3.1 구매자의 ID로 생성된 장바구니(Cart)가 있는지 확인
       3.1.1 장바구니가 있음
           - 장바구니에 이미 상품이 담겨 있는지 확인
               - 이미 담긴 상품이면 사용자에게 알림
               - 담긴 상품이 없으면 상품을 장바구니에 담음
       3.1.2 장바구니가 없음
           - 새로운 장바구니를 생성하고 생성된 장바구니 ID를 받아와 상품을 장바구니에 담음


ProductPurchaseOptios.tsx
(상품 상세 페이지의 하위 컴포넌트에서 작성)

const handleAddCart = async () => {
    // 1. 로그인 상태 확인
    if (!isLogin) { // 1.1 로그인하지 않은 경우
      showModal({
        type: "CONFIRM",
        content: "로그인이 필요한 서비스입니다. \n로그인 하시겠습니까?",
        onConfirm: () => redirectToLogin(),
      });
      return;
    } else if (!isBuyer) { // 2. 판매자로 로그인함
      showModal({
        type: "CONFIRM",
        content: "구매자로 로그인해주세요. \n로그인 하시겠습니까?",
        onConfirm: () => redirectToLoginWithLogout(),
      });
      return;
    }
    // 3. 구매자로 로그인함
    let cartId;
    // 3.1 구매자의 ID로 생성된 장바구니(Cart)가 있는지 확인
    cartId = await getCartId(userId!);
    if (!cartId) { // 3.1.2 장바구니가 없음
      cartId = await createCart(userId!);
    }

    const isInCart = await checkProductInCart(cartId); // 이미 장바구니에 담긴 물건인지 확인
    if (isInCart) { // 장바구니에 이미 상품이 담겨 있는지 확인
      showModal({
        type: "CONFIRM",
        content:
          "이미 장바구니에 담긴 상품입니다. \n장바구니로 이동하시겠습니까?",
        onConfirm: () => redirectToCart(),
      });
      return;
    }
  
	// 담긴 상품이 없으면 상품을 장바구니에 담음
    const { data, error } = await addCartItem({
      cartId,
      productId,
      quantity,
    });
    if (!error) {
      showModal({
        type: "CONFIRM",
        content: "장바구니에 담겼습니다. \n장바구니로 이동하시겠습니까?",
        onConfirm: () => redirectToCart(),
      });
      console.log("장바구니 담긴 상품:", data);
    }
  };
  • showModal은 사용자에게 장바구니에 담을 수 없는 이유를 알려주는 모달을 보여주기 위한 함수로 모달에 대한 자세한 코드는 생략했다.

apis.ts

export async function createCart(userId: string) {
  const supabase = createClient();
  const { data, error } = await supabase
    .from("cart")
    .insert({ user_id: userId })
    .select()
    .single();

  return data.id;
}

export async function getCartId(userId: string) {
  const supabase = createClient();
  const { data, error } = await supabase
    .from("cart")
    .select("id")
    .eq("user_id", userId)
    .single();

  return data?.id;
}

interface CartItem {
  cartId: number;
  productId: number;
  quantity: number;
}
  
export async function addCartItem(cartItem: CartItem) {
  const supabase = createClient();
  const { cartId, productId, quantity } = cartItem;

  const { data, error } = await supabase
    .from("cart_item")
    .insert({ cart_id: cartId, product_id: productId, quantity })
    .select();

  revalidatePath("/cart");

  return { data, error };
}

export async function checkCartItem(cartId: number, productId: number) {
  const supabase = createClient();
  const { data, error } = await supabase
    .from("cart_item")
    .select()
    .eq("cart_id", cartId)
    .eq("product_id", productId);

  if (data!.length > 0) {
    return true;
  }
}
  • revalidatePath를 사용해서 상품이 장바구니에 추가되면 /cart가 재검증되도록했다.

🏄🏻‍♀️ try) revalidateTag를 사용할 수 있을까?

재검증에 있어서revalidatePath 말고도 revalidateTag를 사용할 수도 있다.
revalidateTag를 사용하게되면 해당 tag로 설정된 fetch 요청이 재요청되기 때문에 cart의 데이터를 가져오는 api요청에 태그를 설정하고 장바구니에 상품이 업데이트(추가, 삭제)가 발생하는 경우 revalidateTag를 사용해 재검증을 하고 싶었다.
tag를 설정하려면 fetch에 tag를 함께 전달하는 방식으로 설정하는데 supabase에서 tag를 설정하는 방법에 대해 알아보았으나 정보가 적어 해당 방법이 옳은 방법인지는 확신이 들지 않아 revalidatePath를 사용하는 방식으로 진행하기로 결정했다.

  • createClient의 경우 원래 받아오는 인자가 없었으나 tag를 설정하기위해 tag를 전달하도록 수정해주었다.
  • 해당 방법으로 진행하지는 않았으나 실행했을 때 문제없이(아마도..) 실행되었다.

utils/supabase/server.ts

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

// ➕ add 
export const createFetch =
  (options: Pick<RequestInit, "next" | "cache">) =>
  (url: RequestInfo | URL, init?: RequestInit) => {
    return fetch(url, {
      ...init,
      ...options,
    });
  };

export const createClient = (tag?: string) => {
  const cookieStore = cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      global: { // ➕ add 
        fetch: createFetch({
          next: {
            tags: [`${tag}`],
          },
        }),
      },
      cookies: {
        ...
        },
      },
    }
  );
};
export async function getCartItem(cartId: number) {
  const supabase = createClient("cart"); // ➕ add : "cart" 태그 설정

  const { data: cartItems, error } = await supabase
    .from("cart_item")
    .select()
    .eq("cart_id", cartId);

  ...
}

export async function addCartItem(cartItem: CartItem) {
  const supabase = createClient();
  const { cartId, productId, quantity } = cartItem;

  const { data, error } = await supabase
    .from("cart_item")
    .insert({ cart_id: cartId, product_id: productId, quantity })
    .select();

  revalidateTag("cart"); // ➕ add 

  return { data, error };
}

참고

https://medium.com/@axel.vion71/how-to-se-next-js-14-cache-with-supabase-8756cd7878ab

0개의 댓글