본문 바로가기
TIL

supabase 테이블 조인하여 조회 / 삭제

by dev__log 2024. 3. 21.

오늘은 즐겨 찾는 화장실 페이지 작업을 진행했다. 

마음에 드는 화장실을 북마크 해둔 것을 볼 수 있는 화면인데, 그러기 위해서는 두 가지의 테이블이 필요했다. 

화장실 정보를 가지고 있는 toilet_location 테이블과 유저 별 북마크 정보를 가지고 있는 bookmark 테이블이다. 

 

두 테이블의 정보는 아래와 같다. 

 

bookmark

컬럼명 타입 설명
bookmark_id int [pk] 북마크 아이디
toilet_id int [fk] 화장실 아이디
user_id varchar 유저 아이디

 

toilet_location

컬럼명 타입 설명
toilet_id int [pk] 화장실 아이디
toilet_name varchar 화장실 이름
toilet_address varchar 화장실 주소
toilet_opening_hours varchar 화장실 개방시간
toilet_longitude int 경도
toilet_latitude int 위도
toilet_baby_diaper varchar 귀저기교환대장소

*supabase는 타입이 조금 다르게 적혀있어서 헷갈린다...

조회

기본적인 supabase 조회문은 아래와 같은 문법으로 조회할 수 있다. 

const { data, error } = await supabase
    .from('bookmark')
    .select('*')
    .eq('user_id', user_id);

from에 테이블명을 넣고, select에 조회할 칼럼명을 넣는다. 

eq에 어떤 컬럼에 어떤 값과 같은 데이터를 조회할지 넣어주면 sql에서 where 절에 = 으로 조회하는 것처럼 가능하다. 

위 코드는 bookmark 테이블의 모든 칼럼을 조회하는데 user_id 칼럼에 user_id 변수에 넣은 값과 일치하는 데이터만 조회된다. 

 

 

두 개의 테이블에서 조회하는 문법은 select 문에 조회할 다른 테이블의 이름과 칼럼명을 기재하는 부분을 추가하면 조인처럼 사용할 수 있다. 

외래키(fk)가 지정되어 있으면 supabase에서 자동으로 인식하여 조인하듯이 조회할 수 있게 된다고 한다.

const { data, error } = await supabase
    .from('bookmark')
    .select('*, toilet_location(toilet_name, toilet_address)')
    .eq('user_id', user_id);

 

select 부분에 toilet_location 테이블에서 toilet_name, toilet_address도 조회하도록 하였다. 

 

결과는 아래와 같이 중첩된 객체로 조회된다. 

 

삭제

delete를 사용하여 삭제를 할 수 있다.

const { error } = await supabase.from('bookmark').delete().eq('bookmark_id', bookmarkId);

 

 

할 일

useQuery와 useMutation에 사용할 queryKey를 상수로 관리하고 있는데, 지금은 컴포넌트 파일에 같이 있어서 따로 분리가 필요할 것 같다.

마찬가지로 queryFn, mutationFn에 사용되는 deleteData 함수와 getData 함수도 따로 관리할 예정이다. 

 

 

그리고 tailwind를 이용해서 반응형 레이아웃 작업을 진행 중이다. 북마크 페이지는 어느 정도 되었는데 헤더 컴포넌트 부분도 처리해야 한다. 

 

전체 코드

'use client';

import { supabase } from '@/shared/supabase/supabase';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import React from 'react';
import { PiToiletPaper } from 'react-icons/pi';

//분리 필요
type Bookmark = {
  bookmark_id: number;
  toilet_id: number;
  toilet_location: {
    toilet_address: string;
    toilet_name: string;
  };
  user_id: string;
};

const BookmarkList = () => {
  const USER_ID = '56'; //임시값

  //분리 필요
  const QUERY_KEY_BOOKMARK = 'bookmark'; //bookmark 공통 query key
  const queryClient = useQueryClient();

  //분리 필요
  const deleteData = async (bookmarkId: number) => {
    try {
      const { error } = await supabase.from('bookmark').delete().eq('bookmark_id', bookmarkId);

      if (error) {
        console.error('Error fetching data:', error.message);
        return;
      }
    } catch (error) {
      console.error(error);
    }
  };

  const mutation = useMutation({
    mutationFn: deleteData,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY_BOOKMARK] });
    },
  });

  //분리 필요
  const getData = async (user_id: string): Promise<Bookmark[]> => {
    try {
      const { data, error } = await supabase
        .from('bookmark')
        .select('*, toilet_location(toilet_name, toilet_address)')
        .eq('user_id', user_id);

      if (error) {
        console.error('Error fetching data:', error.message);
        return [];
      }
      console.log(data);
      return data as Bookmark[];
    } catch (error) {
      console.error(error);
      return [];
    }
  };

  const { data } = useQuery<Bookmark[]>({
    queryFn: () => getData(USER_ID),
    queryKey: [QUERY_KEY_BOOKMARK],
  });

  //북마크 취소
  const handleCancelBookmark = (bookmarkId: number) => {
    if (confirm('즐겨찾기를 취소할까요?')) {
      mutation.mutate(bookmarkId);
    }
  };

  return (
    <>
      {data && data.length ? (
        <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 m-4">
          {data?.map((item) => {
            return (
              <li key={item.bookmark_id} className="xl:relative p-4 border rounded-md min-h-32 xl:min-h-40">
                <div className="flex justify-between items-start">
                  <strong className="text-base md:text-xl">
                    <Link href={`/detail_page/${item.toilet_id}`}>{item.toilet_location.toilet_name}</Link>
                  </strong>
                  <button
                    type="button"
                    className="min-w-12 text-gray-400 text-sm"
                    onClick={() => handleCancelBookmark(item.bookmark_id)}
                  >
                    <span className="text-amber-300">★</span> 취소
                  </button>
                </div>
                <p className="text-neutral-600">
                  <Link href={`/detail_page/${item.toilet_id}`}>{item.toilet_location.toilet_address}</Link>
                </p>
                <Link
                  href={`/detail_page/${item.toilet_id}`}
                  className="xl:absolute xl:bottom-4 xl:right-4 text-blue-600 text-sm"
                >
                  자세히 보기
                </Link>
              </li>
            );
          })}
        </ul>
      ) : (
        <div className="flex items-center justify-center flex-col m-4 min-h-60 border rounded-md text-gray-400 font-bold">
          <PiToiletPaper size="50" />
          <p className="mt-4">즐겨찾는 화장실이 없습니다</p>
        </div>
      )}
    </>
  );
};

export default BookmarkList;

 

 

결과 화면

pc 화면

데이터 있을 때

 

데이터 없을 때

 

모바일 화면

데이터 있을 때
데이터 없을 때

 

 

 

 

'TIL' 카테고리의 다른 글

[프로그래머스] 모의고사  (0) 2024.03.28
[supabase] insert, delete 북마크 추가/삭제  (0) 2024.03.25
geolocation, kakao 로컬 rest api 활용  (0) 2024.03.20
Zustand  (2) 2024.03.19
새로운 팀 프로젝트 시작!  (1) 2024.03.18