[프로젝트] Next.js로 장바구니 구현하기 - (5)
기존 구조
삭제까지 모두 완료해서 체크 상품만 결제 되도록 해보려고한다. 처음엔 체크 전에도 장바구니에 담긴 상품의 결제금액이 바로 브라우저에 보였는데 체크 전까진 0원으로 보이도록 하려고한다.
처음 구조는 아래와 같다. CartList는 장바구니 테이블을 보여주는 페이지이다. 그 아래 DataTable과 CartFixedButtons를 임포트하고 있는데 여기서 내가 사용한 shadcn의 데이터 테이블 컴포넌트는 컬럼과 데이터 테이블 파일구조를 분리해서 쓰는 것을 추천해서 그렇게 사용했다.
shadcn의 데이터 테이블 컴포넌트를 처음에 쓸 때 생소했던게 컬럼 컴포넌트는 함수형 컴포넌트가 아니라 배열만 덜렁 들어있었다. 컬럼을 추가하면 테이블에 데이터를 추가할 수 있었지만 함수형 컴포넌트가 아니라 따로 상태로 관리해주거나 하기 힘들었다. 그래서 수량 변경이나 삭제 같은 경우에도 새로고침을 해야 브라우저에 반영됐다. 그래서 아래와 같이 구조를 변경했다.
📌 shadcn 제공 코드
DataColumns.tsx
"use client"
import { ColumnDef } from "@tanstack/react-table"
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: "Amount",
},
]
DataTable.tsx
여기서 coulms을 porps로 받아서 테이블에 데이터를 보이게 하는것.
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
CartList.tsx
내가 선택한 상품의 금액만 CartPriceList에 나와야한다. 여기서 투두리스트와 같은 방식으로 하면 되겠다고 생각했다. Done 영역엔 진행 완료된 투두리스트가 들어가도록하는 로직!! 해다당 유저가 담은 카트 데이터를 가져와서 상태로 관리해주었다.
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const { cartData, isPending, error } = useUserCartData();
const text = '장바구니가 비었어요';
if (isPending) return <Loading />;
if (error) return <div>오류 {error.message}</div>;
return (
<div>
{cartData ? (
<TableDataColumns
selectedItems={selectedItems}
setSelectedItems={setSelectedItems}
/>
) : (
//장바구니 비어있을 경우 디폴트 이미지 표시
<DefaultImage text={text} />
)}
<CartFixedButtons data={cartData ?? []} selectedItems={selectedItems} />
</div>
);
};
Data-table-column-header.tsx
CartList에서 장바구니 데이터에서 id 값만 담은 selectedItem를 props로 받아 체크 선택과 해제하는 코드를 구현했다. 여기서 onCheckedChange에 value는 체크가 됐는지 안됐는지 boolean 값을 가지고있다.
export const TableDataColumns = ({
selectedItems,
setSelectedItems
}: TableProps) => {
const { cartData, isPending, error } = useUserCartData();
if (isPending) return <Loading />;
if (error) return <div>오류 {error.message}</div>;
const columns: ColumnDef<CartItem>[] = [
{
//전체선택
id: 'select',
header: ({ table }) => (
<div className="flex items-center whitespace-nowrap">
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => {
//console.log(value);
const allSelectedItems = value
? cartData?.map((item) => item.product_id)
: [];
setSelectedItems(allSelectedItems as string[]);
table.toggleAllPageRowsSelected(!!value);
}}
aria-label="Select all"
/>
<div className="text-base text-label-strong ml-2 absolute left-10">
{`전체 선택 (${table.getFilteredSelectedRowModel().rows.length}/${
table.getFilteredRowModel().rows.length
})`}
</div>
</div>
),
//부분선택
cell: ({ row }) => (
<Checkbox
checked={
selectedItems.length > 0
? selectedItems.includes(row.getValue('product_id'))
: false
}
onCheckedChange={(value) => {
setSelectedItems((prev) => {
if (value) {
return [...prev, row.getValue('product_id')];
} else {
return prev.filter((id) => id != row.getValue('product_id'));
}
});
}}
aria-label="Select row"
style={{ transform: 'translate(0, -130%)' }}
/>
),
enableSorting: false,
enableHiding: false
},
];
return (
<DataTable
columns={columns}
data={cartData ?? []}
selectedItems={selectedItems}
/>
);
};
체크 선택과 해제 로직은 아래 코드를 참고했다.
CartPriceList.tsx
기존 코드에서 추가한건 if (selectedItems.includes(item.product_id ?? '')) 이것뿐이다.
'use client';
import { Tables } from '@/types/supabase';
import { useEffect, useState } from 'react';
interface CartProps {
data: Tables<'cart'>[] | null;
selectedItems: string[];
}
const DELIVERY_FEE = 2500;
const COUPON = 2000;
export const CartPriceList = ({ data, selectedItems }: CartProps) => {
const [totalAmount, setTotalAmount] = useState(0);
useEffect(() => {
const calculator =
data?.reduce((acc, item) => {
if (selectedItems.includes(item.product_id ?? '')) {
const price = item.product_price ?? 0;
const quantity = item.count ?? 0;
return acc + price * quantity;
}
return acc;
}, 0) || 0;
setTotalAmount(calculator);
}, [data, selectedItems]);
if (!data || (data.length === 0 && !selectedItems)) {
return null;
}
//총 결제금액
const totalPrice = totalAmount + DELIVERY_FEE - COUPON;
};
CartFixedButtons.tsx
<PayButton product={product} orderNameArr={orderNameArr} /> product에서 아래와 같은 오류가 났다. 타입 오류인데 null 값이 포함된 배열이 '{ name: string | null; amount: number; quantity: number; id?: string | undefined; }' 이런 특정 타입에 할당될 수 없다는 것 같다. 오늘도 난 구글링을 한다..,,
'({ name: string | null; amount: number; quantity: number; } | null)[]' 형식은 'Products' 형식에 할당할 수 없습니다. '{ name: string | null; amount: number; quantity: number; } | null' 형식은 '{ name: string | null; amount: number; quantity: number; id?: string | undefined; }' 형식에 할당할 수 없습니다. 'null' 형식은 '{ name: string | null; amount: number; quantity: number; id?: string | undefined; }' 형식에 할당할 수 없습니다.
ts(2322)
export const CartFixedButtons = ({ data, selectedItems }: CartButtonProps) => {
const totalPrice =
data?.reduce((acc, item) => {
if (selectedItems.includes(item.product_id ?? '')) {
const price = item.product_price ?? 0;
const quantity = item.count ?? 0;
return acc + price * quantity;
}
return acc;
}, 0) || 0;
const totalAmount = totalPrice + DELIVERY - COUPON;
const orderNameArr = data
.map((item) => {
if (selectedItems.includes(item.product_id as string)) {
return item.product_name;
}
return null;
})
.filter((name): name is string => name !== null);
// 전달 데이터 형식
// {
// name: "청송 사과",
// amount: 8000,
// quantity: 3,
// }
const product = data
.map((item) => {
if (selectedItems.includes(item.product_id as string)) {
return {
name: item.product_name,
amount: (item.product_price ?? 0) * (item.count ?? 0),
quantity: item.count ?? 0
};
}
return null; //장바구니 선택 상품 외 null 처리
})
타입가드로 is를 사용했는데 사용한 이유는 특정 타입을 체크하는 함수에서 반환되는 값이 true 이면 해당 함수를 사용하는 블록 안에서도 인자로 받은 값의 타입을 특정 타입으로 확정시켜 줄 수 있도록 하는 기능을 한다고한다. 원래 특정 변수를 함수로 넘겨서 타입을 체크하게 되면 그 함수를 사용하고 있는 스코프에서는 타입 축소가 안되어 불편했었는데, 이 키워드를 활용하면 그 부분을 해결 할 수 있다고해서 사용했다 ㅎ is 는 typescript 에서 지원하고 있는 키워드라고 한다.
수정 코드
item이 null이 아닌 경우에만 해당 객체의 타입으로 축소될 수 있도록 했다.
const product = data
.map((item) => {
if (selectedItems.includes(item.product_id as string)) {
return {
name: item.product_name,
amount: (item.product_price ?? 0) * (item.count ?? 0),
quantity: item.count ?? 0
};
}
return null; //장바구니 선택 상품 외 null 처리
})
.filter(
(
item
//타입 에러 : 타입 가드로 타입 축소
): item is { name: string | null; amount: number; quantity: number } =>
item != null
);
프로젝트 적용
우하하 완성!