[프로젝트] Next.js 장바구니 구현하기 - (4)

가현님 덕에 최고심에 빠져버렸다.. 채김져.. 미루고 미루던 삭제 구현하기. 그래도 수량 버튼을 useMutation으로 처리하면서 힌트를 얻어서 삭제도 useMutation을 통해 데이터를 업데이트하려고 한다.

ejunyang.tistory.com

 


 

기존 구조

삭제까지 모두 완료해서 체크 상품만 결제 되도록 해보려고한다. 처음엔 체크 전에도 장바구니에 담긴 상품의 결제금액이 바로 브라우저에 보였는데 체크 전까진 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}
    />
  );
};

 

체크 선택과 해제 로직은 아래 코드를 참고했다.

 

Checkbox

A control that allows the user to toggle between checked and not checked.

ui.shadcn.com

 

 

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 에서 지원하고 있는 키워드라고 한다.

 

타입 가드와 타입 축소에 대하여

안녕하세요! 최근에 타입 가드와 타입 축소에 대한 용어를 처음 들어봐서 해당 용어에 대해 알아보려고 합니다. 타입 가드 타입 가드(Type Guard)는 컴파일러가 타입을 예측할 수 있도록 타입의 범

funveloper.tistory.com

 

 

수정 코드

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
    );

 

 

 

 

프로젝트 적용

우하하 완성!

 

+ Recent posts