장바구니에 담은 상품 수량 추가/감소 기능 구현하던 중 추가하면 아래 결제 금액도 변경이 되어야하는데 변경이 되지 않고 새로고침해야 변경이 되었다.. useState로 화면에 바로 렌더링되도록 했는데 먹히질 않았다.. 열받아서 41번이나 눌러본 건에 대하여.

 

 

파일구조

장바구니 ui는 shadcn ui - Data Table을 사용했다. 공식문서에서 말하는대로 컬럼 컴포넌트와 테이블 컴포넌트를 분리해서 만들었다. 근데 분리해서 만들었더니 props 전달에 문제 발생.. DataTable이 CartList 컴포넌트 안에 있었기 때문.

📦cart
 ┣ 📂_components
 ┃ ┣ 📂data-table
 ┃ ┃ ┣ 📜CountButton.tsx
 ┃ ┃ ┣ 📜Data-table-column-header.tsx
 ┃ ┃ ┗ 📜DataTable.tsx
 ┃ ┣ 📜CartFixedButtons.tsx
 ┃ ┣ 📜CartList.tsx
 ┃ ┣ 📜CartPriceList.tsx
 ┃ ┗ 📜DefaultCart.tsx
 ┣ 📜layout.tsx
 ┗ 📜page.tsx
 

Data Table

Powerful table and datagrids built using TanStack Table.

ui.shadcn.com

 

Data-table-column-header.tsx

해당 컴포넌트에 수량 버튼 컴포넌트를 넣어서 DataTable에 count를 props로 줘도 요지부동인 이유~ㅎㅎ

  {
    accessorKey: 'count',
    header: '',
    cell: ({ row }) => {
      return (
        <CountButton
          product_id={row.getValue('product_id')}
          counts={row.getValue('count')}
        />
      );
    }
  },

 

CartList.tsx

 

수파베이스의 카트데이터를 가지고와서 useQuery로 서버상태를 관리했다. 데이터 테이블에서 변경 이벤트는 수량 버튼을 클릭했을 때 생기는 이벤트뿐이라 useState를 통해 count 값이 변경될때마다 데이터를 리패치하는 방법도 생각해 봤으나 컬럼 컴포넌트에 count가 있어 DataTable 컴포넌트에 props로 내려줘도 아무 소용이 없었다. 그래서 생각해낸 방법이 리액트 쿼리로 상태를 관리하고 있으니 mutation을 사용해서 업데이트를 하자였다. 그럼 invalidateQueries 로 무효화하기 때문에 데이터를 다시 가져오니까 CartList 에서 다시 리패치하지 않아도 된다.

'use client';

import { CartFixedButtons } from './CartFixedButtons';
import { DefaultCart } from './DefaultCart';
import { DataTable } from './data-table/DataTable';
import { columns } from './data-table/Data-table-column-header';
import { useQuery } from '@tanstack/react-query';
import supabase from '@/utils/supabase/client';
import Loading from '@/components/common/Loading';

export const CartList = () => {
  const {
    data: cartData,
    isPending,
    error
  } = useQuery({
    queryKey: ['cart'],
    queryFn: async () => {
      const {
        data: { user },
        error: userError
      } = await supabase.auth.getUser();

      if (!user) return [];
      if (userError) console.error(userError.message);

      const { data, error } = await supabase
        .from('cart')
        .select('*, product:product_id(*)')
        .eq('user_id', user.id);

      if (error) throw new Error(error.message);

      return data.sort((a, b) => {
        const idA = a.product_id ?? '';
        const idB = b.product_id ?? '';
        return idA.localeCompare(idB);
      });
    }
  });

  if (isPending) return <Loading />;
  if (error) return <div>오류 {error.message}</div>;

  return (
    <div>
      {cartData?.length > 0 ? (
        <DataTable columns={columns} data={cartData} />
      ) : (
        <DefaultCart />
      )}

      <CartFixedButtons data={cartData} />
    </div>
  );
};

 

 

CountButton.tsx

서버에 수량을 업데이하는 함수를 만들었다. 

  const updateCountInDatabase = async (newCount: number) => {
    const { error } = await supabase
      .from('cart')
      .update({
        count: newCount
      })
      .eq('product_id', product_id);

    if (error) {
      console.log({ error });
      alert('수량이 업데이트되지 않았습니다.');
      throw new Error('수량 업데이트 실패');
    }
  };

 

 

수량 추가

const addCountMutation = useMutation({
    mutationFn: async () => {
      const newCount = counts + 1;
      await updateCountInDatabase(newCount);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['cart']
      });
    }
  });

 

 

수량 빼기(?)

최소 수량이 1이기 때문에 조건문을 넣어주었다.

const subCountMutation = useMutation({
    mutationFn: async () => {
      if (counts > 1) {
        const newCount = counts - 1;
        await updateCountInDatabase(newCount);
      }
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['cart']
      });
    }
  });

 

 

onClick에서 오류가 났는데 [ TypeScript Error ] '( ) => void' 형식은 'MouseEventHandler<HTMLButtonElement>' 형식에 할당할 수 없습니다. 타입 오류가 났다.. 난 타입오류가 정말 싫다.. 그래서 탭에서 지운다.. 왜냐면 탭에서 지우면 빨갰던 파일이 다시 정상으로 돌아오니까..........𖤐

return (
    <div className="flex gap-x-2 items-center">
      <button onClick={SubCountMutation.mutate} className="rounded-sm">
        -
      </button>
      <span>{counts}</span>
      <button onClick={addCountMutation.mutate} className="rounded-sm">
        +
      </button>
    </div>
  );

 

 

구글링 하다보니 나와 같은 오류를 찾았다. Event에 맞는 함수를 받아야 하는데 함수 이름를 바로 넣어버리니 함수의 타입과 일치하지 않아서 생긴 오류라고 한다. 아래와 같이 변경하면 빨갰던 줄이 사라진다(편안)

return (
    <div className="flex gap-x-2 items-center">
      <button onClick={() => subCountMutation.mutate()} className="rounded-sm">
        -
      </button>
      <span>{counts}</span>
      <button onClick={() => addCountMutation.mutate()} className="rounded-sm">
        +
      </button>
    </div>
  );

 

 

 

 

프로젝트 적용

요지부동하던 결제금액이...!!! 변한다. 근데 수량 버튼을 눌렀을 때 좀 느리게 반영되어서 이 부분은 optimistic update를 적용해보려고한다.

 

 

이번주 월요일부터 개발작업에 들어갔다. 와이어프레임이 거의 나와서(혜원님의 열일) 개발을 바로 시작할 수 있었다. 내가 맡은 기능은 아래와 같다. 우선 월요일에 특산물 전체보드와 상세페이지를 대충 끝냈고, 화요일에 피그마에 나와있는대로 섬세하게 ui를 조정했다.

  • 특산물 판매 전체 보드
  • 특산물 상세페이지
  • 장바구니
  • 메인페이지

 

대망의 장바구니 .. 회원가입 후 쿠폰을 𖤐발급𖤐 해줘서 장바구니에 담은 상품을 결제할 때 사용해야한다.(무조건) 쿠폰함을 따로 만들어 선택하는 것이 어떠냐는 의견이 나왔지만 .. 내가 생각해도 그것이 사용자 입장에서 너무 좋겠지만 .. 시간대비 효율이 너무 떨어질 것 같았다.. 우리한테 남은 시간이 그렇게 많지 않기 때문에 추후 선택 사항으로 남겨놓았다. 근데 뭔가 하면 할수록...... 기능들이 더 생겨요... 왜...

 

 


 

 

🥑 상세페이지

상세페이지에서 로직은 아래와 같다.

장바구니 담기 => '장바구니로 이동하시겠습니까?'
문구 출력과 확인 버튼을 누르면 장바구니로 이동

이미 같은 상품이 담겼다면 => '이미 담긴 상품입니다. 장바구니로 이동하시겠습니까?' 
문구 출력과 확인 버튼을 누르면 장바구니로 이동

 

🧺 장바구니

장바구니에 담은 상품이 없다면 => 디폴트 이미지 출력 + '상품을 담아주세요' 버튼

장바구니에 상품이 담겨있다면 => 상품이미지, 상품명, 가격, 수량 + '바로 구매하기' 버튼

 

 

 

 

 

장바구니 담기

우선 장바구니는 회원 전용으로 운영할 것이기 때문에 사용자가 로그인 했는지 확인한다. 로그인했다면 사용자의 데이터를 가져온다.

 const {
      data: { user },
      error: userError
    } = await supabase.auth.getUser();
    if (userError || !user) {
      alert('로그인을 해주세요.');
      return;
    }

 

해당 사용자 id로 담은 상품이 있는지 확인한다.

try {
      const { data: cartData, error: cartError } = await supabase
        .from('cart')
        .select('*')
        .eq('product_id', food.product_id)
        .eq('user_id',user.id)
        .maybeSingle();

      if (cartError) {
        alert('장바구니에 담기 중 에러가 발생했습니다.');
        return;
      }

 

만약 상품이 없다? 그럼 insert 하자. 들어가야하는 데이터는 아래와 같다.

 if (!cartData) {
        const { error: insertError } = await supabase.from('cart').insert({
          product_id: food.product_id,
          image: food.title_image,
          product_name: food.food_name,
          product_price: food.price,
          user_id: user.id
        });
        if (insertError) {
          alert('장바구니에 상품이 담기지 않았습니다.');
          return;
        }
      } else {
        const isConfirmed = confirm(
          '이미 장바구니에 담긴 상품입니다. 장바구니로 이동하시겠습니까?'
        );
        if (isConfirmed) {
          router.push('/cart');
        }
        return;
      }
    } catch (error) {
      alert('장바구니 담기 중 오류가 발생했습니다.');
      console.error(error);
    }

    const isConfirmed = confirm(
      '장바구니에 담겼습니다. 장바구니로 이동하시겠습니까?'
    );
    if (isConfirmed) {
      router.push('/cart');
    }
  };

 

 

 

 

장바구니 페이지

장바구니 페이지에 있는 데이터들을 가져온다.

const {
    data: cartData,
    isPending,
    error
  } = useQuery({
    queryKey: ['cart'],
    queryFn: async () => {
      const {
        data: { user },
        error: userError
      } = await supabase.auth.getUser();

      if (!user) return [];
      if (userError) console.error(userError.message);

      const { data, error } = await supabase
        .from('cart')
        .select('*, product:product_id(*)')
        .eq('user_id', user.id);

      if (error) throw new Error(error.message);

      return data;
    }

Next.js 프로젝트 생성

npx create-next-app@latest

 

 

supabase 테이블 생성

 

 

superbase client

yarn add @supabase/supabase-js

 

📁 utils / 📁 supabase / client.ts

import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL! as string,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! as string
  )
}

export default supabase;

 

 

supabase type 설정

supabase package 다운받기

npm i supabase@">=1.8.1" --save-dev
npx supabase login

 

supabase 파일 생성

npx supabase init

 

types폴더에 supabase.ts 파일 생성

$PROJECT_REF 에는 프로젝트 이름을 써주면 된다.

npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts

 

 

자동 완성

 

supabaseClient.ts 생성

createClient의 타입으로 Database를 반드시 줘야한다

import { Database } from '@/types/supabase';
import { createBrowserClient } from '@supabase/ssr';
import { SupabaseClient } from '@supabase/supabase-js';

export function createClient(): SupabaseClient<Database> {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

//client component 보일러 플레이트 방지
const supabase = createClient();

export default supabase;

 

 

 

 

📌 스크립트  처리

타입이 업데이트 될 경우 아래 명령어를 다시 입력해줘야한다. 그러기에는 너무 긴 명령어.. pakage.json에 들어가 script를 추가하자.

npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts

 

 

$PROJECT_ID 에는 프로젝트 id값을 넣어주면 된다. 프로젝트 ID값은 supabase의 Project Setting에서 확인할 수 있다.

"gen": "npx supabase gen types typescript --project-id \"$PROJECT_ID\" --schema public > types/supabase.ts"

 

지정한 스크립트를 아래 명령어를 통해 실행할 수 있고, 이 명령어로 타입을 업데이트 시켜줄 수 있다.

npm run gen

 

 

 

[프로젝트] Next.js 심화 프로젝트 (1) - 기획편

사전 기획 - 마인드맵  선정 주제각 지역별 전통 시장을 소개하고 특산물을 판매하는 웹사이트를 제작하기로 했다.  레퍼런스 리서치  페이지별 세부 기능

ejunyang.tistory.com

 

 

기능 선정

 

이번에 맡은 기능은 아래와 같다.

  • 메인페이지
  • 특산물 전체 보드 페이지
  • 특산물 디테일 페이지
  • 장바구니
  • 선택사항 => 체크한 상품만 결제 페이지로 넘어가기

실시간 채팅을 구현해보고싶었는데 사다리타기에서 졌다.. 하지만 맡으신 혜미님은 훨씬 더 잘 구현하실 것 같다 넘 든든한 팀원,, 실시간 채팅 기능은 개인 프로젝트 때 적용해보아야겠다!

 

 

[프로젝트] 공공데이터포털 API 가져오기

심화 프로젝트 주제로 전통시장을 홍보하는 웹사이트를 구현하기로 했다. 원래는 공공데이터포털에 '전통시장표준데이터규격' api를 사용하려고 했는데 계속해서 SERVICE KEY IS NOT REGISTERED ERROR 이

ejunyang.tistory.com

 

우선 전통시장을 홍보하는 웹사이트이기때문에 전통시장 api가 잘 불러와지는지 확인했다. 아주 잘 가져와지는 것 확인~ 대신 api에 이미지가 없어서 이건 네이버 이미지 검색 api를 활용하려고 한다.

 

 

 

supabase 세팅

오늘 DB 세팅까지 마쳤다. 우선 연결해야하는 테이블들은 연결해두었는데 작업하다가 분명 테이블을 만질 일이 있을 것 같다.. 팀원들이랑 우선적으로 초기세팅은 마친 상태이다.

 

 

 

와이어프레임

어제 웹 와이어프레임을 모두 완료했는데, 어젯 밤 앱으로 먼저 시작해야한다고 전달받았다.. 그게 기본적인 순서라고.. 전체 와이어 프레임은 아니지만 앱 와이어프레임까지 모두 끝냈다.

 

 

 

심화 프로젝트 주제로 전통시장을 홍보하는 웹사이트를 구현하기로 했다. 원래는 공공데이터포털에 '전통시장표준데이터규격' api를 사용하려고 했는데 계속해서 SERVICE KEY IS NOT REGISTERED ERROR 이라는 오류가 떴다. 인증키를 재발급 받아보기도하고, 인코딩된 키값을 넣어 호출하면 해당 키값이 한번더 인코딩되어 온다고해서 decodeURIComponent() 메서드를 사용해서 디코딩 작업을 했는데도 오류가 사라지지 않았다.. 공공데이터 키값으로 해당 에러를 경험한 사람이 많아서 우선은 구글에 나오는 방법들로 시도해봤지만 에러가 고쳐지지 않아 공공데이터포털에 문의를 넣은 상태이다. 키 값의 문제가 아니라고 하면 에러 이유에 대해서 더 알아봐야할 것 같다.

그렇게 돌고 돌아 다른 api 를 찾다가 원래 가지고 오려는 api보다 더 많은 정보가 있는 '전통시장현황' api를 발견했다. 

 

 

 

 

활용신청

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

 

 

활용신청을 누르면 해당 api를 어떤 목적으로 사용할껀지 물어보는 창이 나온다. 목적을 입력하고 신청하면 완료.

 

 

서비스를 신청하면 신청내역에 인코딩한 키와 디코딩 키 값이 있다. 

 

 

활용명세

아래로 조금 내리면 OPEN API 호출 버튼이 있다. 이걸로 먼저 테스트할 수 있다.

 

 

여기 나오는 url을 사용하면 된다.

서버응답

키값을 넣지 않고 호출하려고 하면 아래와 같은 서버 에러가 난다.

 

응답

인증키를 넣어주면 제대로 호출되는 것을 확인 할 수 있다.

 

 

 

📁 api / 📁 market / 📑 route.ts

환경변수에 키값을 넣고 요청 url에 넣고 호출해줘야한다. 마이페이지에 뜨는 End Point 주소와는 다르다.

import { NextRequest, NextResponse } from 'next/server';

const MARKET_URL =
  'api_url';

export const GET = async (request: NextRequest) => {
  try {
    const serviceKey = encodeURIComponent('serviceKey');
    const apiKey = process.env.NEXT_PUBLIC_API_KEY;
    const response = await fetch(
      `${MARKET_URL}?${serviceKey}=${apiKey}&page=1&perPage=1388`
    );
    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: '데이터를 가져오지 못했습니다.' },
      { status: 500 }
    );
  }
};

사전 기획 - 마인드맵

 

 

선정 주제

각 지역별 전통 시장을 소개하고 특산물을 판매하는 웹사이트를 제작하기로 했다.

 

 

레퍼런스

 

리서치

 

 

페이지별 세부 기능

 

 

프로젝트 선택구현 사항 중 ui 컴포넌트를 모아 놓은 라이브러리를 사용하는 것이 있어서 한번 프로젝트에 적용해보았다. 다들 alert를 많이 사용하고 있어서 alert ui를 통일해보장

 

shadcn/ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.

ui.shadcn.com

 

 

 

설치

npx shadcn-ui@latest add alert

 

 

사용법

기본적으로 제공하고 있는 코드를 넣어주면 사용 완료..

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

<Alert>
  <Terminal className="h-4 w-4" />
  <AlertTitle>Heads up!</AlertTitle>
  <AlertDescription>
    You can add components and dependencies to your app using the cli.
  </AlertDescription>
</Alert>

 

 

Tailwind CSS

<Alert className="z-50 bg-slate-50 fixed w-1/3 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white">
        <AlertTitle className="text-xl font-bold">{alertTitle}</AlertTitle>
        <AlertDescription className="mt-3">{alertMessage}</AlertDescription>
        <AlertDescription className="text-center mt-3">
          <button
            className="bg-main-color p-2 px-3 rounded-md text-white"
            onClick={() => setAlert(false, "", "")}
          >
            확인
          </button>
        </AlertDescription>
      </Alert>

 

 

 

적용

const {
    data: { user },
  } = await supabase.auth.getUser();

  // TODO: /api로 시작하는 주소는 무시
  if (
    !user &&
    !request.nextUrl.pathname.startsWith("/api") &&
    !request.nextUrl.pathname.startsWith("/auth")
  ) {
    // no user, potentially respond by redirecting the user to the login page
    const url = request.nextUrl.clone();
    url.pathname = "/auth/login";
    return NextResponse.redirect(url);
  }

 

Supabase의 getUser 메서드를 사용하여 현재 인증된 사용자의 정보를 가져온다.

  • user 객체가 존재하지 않는 경우 (즉, 사용자가 인증되지 않은 경우)
  • 요청된 URL 경로가 /api로 시작하지 않는 경우
  • 요청된 URL 경로가 /auth로 시작하지 않는 경우

조건을 모두 만족하면, 사용자를 로그인 페이지로 리다이렉션한다. NextResponse.redirect(url) 메서드는 사용자를 /auth/login 경로로 리다이렉션 시킨다.

+ Recent posts