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