코딩/React

Supabase와 NextJS로 블로그 만들기 - Search

드리프트 2022. 3. 1. 10:48
728x170

안녕하세요?

 

이번 시간에는 Supabase 블로그 마지막 시간이 Search 기능 추가 시간입니다.

 

블로그 포스트의 생성, 조회, 업데이트, 삭제 기능을 완성했는데요.

 

남은 건 바로 검색 기능입니다.

 

/pages/post/search.tsx 파일로 진행할 예정입니다.

 

일단 User-Authenticated 템플릿을 아래와 같이 준비합시다.

import React, { useState } from "react";
import Link from "next/link";
import { GetServerSideProps } from "next";
import { useMessage } from "../../lib/message";
import { useFormFields } from "../../lib/utils";
import { supabase } from "../../lib/supabase";
import { User } from "@supabase/supabase-js";
import classNames from "classnames";

const SearchPost = ({ user }: { user: User }) => {
  const { messages, handleMessage } = useMessage();

  return (
    <div className="flex flex-col items-center justify-start py-4 min-h-screen">
      <h2 className="text-lg my-4">
        Hello, {user && user.email ? user.email : "Supabase User!"}
      </h2>
      {messages &&
        messages.map((message, index) => (
          <div
            key={index}
            className={classNames(
              "shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
              message.type === "error"
                ? "bg-red-500 text-white"
                : message.type === "success"
                ? "bg-green-300 text-gray-800"
                : "bg-gray-100 text-gray-800"
            )}
          >
            {message.message}
          </div>
        ))}

      <h1 className="py-3 font-semibold text-2xl text-blue-600">
        Search Posts
      </h1>

};

export default SearchPost;

export type NextAppPageUserProps = {
  props: {
    user: User;
    loggedIn: boolean;
  };
};

export type NextAppPageRedirProps = {
  redirect: {
    destination: string;
    permanent: boolean;
  };
};

export type NextAppPageServerSideProps =
  | NextAppPageUserProps
  | NextAppPageRedirProps;

export const getServerSideProps: GetServerSideProps = async ({
  req,
}): Promise<NextAppPageServerSideProps> => {
  const { user } = await supabase.auth.api.getUserByCookie(req);

  if (!user) {
    return {
      redirect: {
        destination: "/auth",
        permanent: false,
      },
    };
  }

  return {
    props: {
      user,
      loggedIn: !!user,
    },
  };
};

이제 Search를 위한 Form 필드 관련 State를 준비해야겠죠.

type FormFieldProps = {
  title: string;
  id?: number;
};

const FORM_VALUES: FormFieldProps = {
  title: "",
};

const SearchPost = ({ user }: { user: User }) => {
  const { messages, handleMessage } = useMessage();

  const [values, handleChange, resetFormFields] =
    useFormFields<FormFieldProps>(FORM_VALUES);

  const [posts, setPosts] = useState<FormFieldProps[]>(null);

FormFieldProps에는 title을 필수, id를 옵셔널로 지정했습니다.

 

왜냐하면 useFormFields에서 form 관리를 할 때는 title로만 검색할 예정이거든요.

 

그리고 supabase.select로 검색된 post를 불러왔을 때는 title, id 두 개를 불러올 예정입니다.

 

그래서 id 부분을 옵셔널로 지정했습니다.

 

그리고 posts, setPosts 처럼 검색된 post 결과를 배열로 지정했습니다.

 

이제 검색방법에 대해 알아볼까요?

const handleSumbit = async (event: React.FormEvent) => {
    event.preventDefault();
    const { data: posts, error } = await supabase
      .from("posts")
      .select("title, id")
      .like("title", `%${values.title}%`)
      .order("id");

    if (error) {
      console.log(error);
      handleMessage({
        message: "Error : Search Posts",
        type: "error",
      });
    } else {
      //   console.log(posts);
      setPosts(posts);
    }
    resetFormFields();
  };

handleSubmit 함수를 살펴봅시다.

 

form의 values.title에 있는 값을 supabase 검색을 이용하는데요.

 

title의 정확한 값을 검색하려면 textSearch를 써도 되지만, 저 같은 경우는 검색어가 들어가 있는 모든 Post를 검색하기를 원해서 like 메써드를 사용했습니다.

 

supabase.select.like.order 방식인데요.

 

like 메써드 안에는 "title"처럼 어느 칼럼에서 검색할지 먼저 지정하고,

 

두 번째처럼

 

여기서 주의할 점은  '%${values.title}%' 이렇게 쓰시면 안 됩니다.

 

SQL 명령어로 볼 때 다음과 같이 작동해야 하기 때문입니다.

select title, id from posts where title like %query%;

query 앞과 끝에 %가 와야 하기 때문에 따옴표를 넣으면 안 됩니다.

 

마지막으로 UI의 Form 부분입니다.

return (
    <div className="flex flex-col items-center justify-start py-4 min-h-screen">
      <h2 className="text-lg my-4">
        Hello, {user && user.email ? user.email : "Supabase User!"}
      </h2>
      {messages &&
        messages.map((message, index) => (
          <div
            key={index}
            className={classNames(
              "shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
              message.type === "error"
                ? "bg-red-500 text-white"
                : message.type === "success"
                ? "bg-green-300 text-gray-800"
                : "bg-gray-100 text-gray-800"
            )}
          >
            {message.message}
          </div>
        ))}

      <h1 className="py-3 font-semibold text-2xl text-blue-600">
        Search Posts
      </h1>

      <form
        onSubmit={handleSumbit}
        className="bg-white w-3/4 md:w-1/2 shadow-md rounded px-8 pt-6 pb-8 mb-4"
      >
        <div className="mb-4">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="title"
          >
            Title
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="title"
            name="title"
            required
            value={values.title}
            onChange={handleChange}
          />
        </div>

        <div className="flex gap-2">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            Search
          </button>
        </div>
      </form>

      {!!posts &&
        posts.map((post, index) => (
          <section className="text-center lg:text-left">
            <div className="max-w-screen-xl px-4 py-4 mx-auto sm:px-6 lg:px-8 lg:items-end lg:justify-between lg:flex">
              <div className="max-w-xl mx-auto lg:ml-0">
                <Link href={`/post/${post.id}`}>
                  <a className="mt-2 text-3xl font-bold sm:text-4xl text-gray-500">
                    {post?.title}
                  </a>
                </Link>
              </div>
            </div>
          </section>
        ))}
    </div>
  );

실행 결과를 볼까요?

 

검색 결과를 클릭하면 Post의 상세 페이지로 이동될 겁니다.

 

이상으로 Supabase 블로그 시스템을 완성했고요.

 

여러분께서 직접 검색 시 content에서도 검색할 수 있게 한번 코드를 수정해 보시는 것도 좋을 듯 싶습니다.

그리드형