본문 바로가기

카테고리 없음

Fiesta Tour_4일차 ( Supabase 연동 및 리뷰CRUD 구현 )

Supabase 세팅법

폴더구조

📦src
 ┣ 📂app
 ┃ ┣ 📂(providers)
 ┃ ┃ ┣ 📂(root)
 ┃ ┃ ┃ ┣ 📂(mypage)
 ┃ ┃ ┃ ┃ ┣ 📂my-page
 ┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
 ┃ ┃ ┃ ┃ ┗ 📂review-page
 ┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
 ┃ ┃ ┃ ┣ 📜layout.tsx
 ┃ ┃ ┃ ┗ 📜page.tsx
 ┃ ┃ ┣ 📂_providers
 ┃ ┃ ┃ ┗ 📜QueryProvider.tsx
 ┃ ┃ ┗ 📜layout.tsx
 ┃ ┣ 📂api
 ┃ ┃ ┗ 📂reviews
 ┃ ┃ ┃ ┗ 📜route.ts
 ┃ ┣ 📂login
 ┃ ┃ ┗ 📜page.tsx
 ┃ ┣ 📜globals.css
 ┃ ┗ 📜layout.tsx
 ┣ 📂assets
 ┃ ┗ 📜a.ts
 ┣ 📂components
 ┃ ┗ 📜a.ts
 ┣ 📂hooks
 ┃ ┗ 📜a.ts
 ┣ 📂lib
 ┃ ┗ 📜a.ts
 ┣ 📂services
 ┃ ┗ 📜a.ts
 ┣ 📂types
 ┃ ┗ 📜supabase.ts
 ┣ 📂utils
 ┃ ┗ 📂supabase
 ┃ ┃ ┣ 📜client.ts
 ┃ ┃ ┣ 📜middleware.ts
 ┃ ┃ ┣ 📜server.ts
 ┃ ┃ ┗ 📜service.ts
 ┗ 📜middleware.ts

 

코드

1. client.ts

import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

 

  • 역할: 브라우저 클라이언트를 생성하는 함수입니다.
  • 구성 요소:
    • createBrowserClient: Supabase의 ssr 패키지에서 가져온 함수로, 브라우저 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
    • process.env.NEXT_PUBLIC_SUPABASE_URL 및 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY: Supabase 프로젝트에 접근하기 위한 환경 변수입니다.
  • 설명: 이 함수는 브라우저 환경에서 Supabase 클라이언트를 초기화하여 사용자가 Supabase 서비스를 사용할 수 있도록 설정합니다. 주로 브라우저 사이드에서 Supabase API를 호출할 때 사용됩니다.

 

2. middleware.ts

import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
          supabaseResponse = NextResponse.next({
            request
          });
          cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
        }
      }
    }
  );

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

  if (user && (request.nextUrl.pathname.startsWith('/login') || request.nextUrl.pathname.startsWith('/signup'))) {
    return NextResponse.redirect(request.nextUrl.origin);
  }

  return supabaseResponse;
}

 

  • 역할: 서버 클라이언트를 생성하고, 세션을 업데이트하는 미들웨어 함수입니다.
  • 구성 요소:
    • createServerClient: Supabase의 ssr 패키지에서 가져온 함수로, 서버 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
    • NextRequest 및 NextResponse: Next.js에서 서버 사이드 미들웨어를 처리하는 객체들입니다.
    • supabase.auth.getUser(): 현재 인증된 사용자를 가져오는 함수입니다.
    • 리다이렉션 로직: 사용자가 로그인된 상태에서 로그인 페이지나 회원가입 페이지에 접근할 때 홈으로 리다이렉션합니다.
  • 설명: 이 미들웨어 함수는 서버 사이드에서 Supabase 클라이언트를 초기화하고, 사용자의 세션을 업데이트합니다. 특정 경로에 따라 리다이렉션을 처리하여 올바른 페이지로 사용자를 안내합니다.

 

3. server.ts

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export function createClient() {
  const cookieStore = cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  );
}

 

  • 역할: 서버 클라이언트를 생성하는 함수입니다.
  • 구성 요소:
    • createServerClient: Supabase의 ssr 패키지에서 가져온 함수로, 서버 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
    • cookies: Next.js의 헤더에서 쿠키를 관리하는 객체입니다.
    • cookieStore: 쿠키를 저장하고 관리하는 로직을 포함합니다.
  • 설명: 이 함수는 서버 환경에서 Supabase 클라이언트를 초기화하고, 요청에 따라 쿠키를 설정하거나 가져오는 기능을 제공합니다. 서버 컴포넌트에서 Supabase 클라이언트를 사용할 때 유용합니다.

4. service.ts

import { createClient } from '@/utils/supabase/client';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

const validateEmail = (email: string): boolean => {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
};

const validatePassword = (password: string): boolean => {
  const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return regex.test(password);
};

export const handleLogin = async (
  email: string,
  password: string,
  router: any,
  setError: (message: string) => void
) => {
  const supabase = createClient();

  if (!email) {
    return toast.error('이메일을 입력하세요!');
  }
  if (!password) {
    return toast.error('비밀번호를 입력하세요!');
  }

  const { data, error: signInError } = await supabase.auth.signInWithPassword({
    email,
    password
  });

  if (signInError) {
    if (signInError.message.toLowerCase().includes('invalid login credentials')) {
      toast.error('잘못된 로그인 자격 증명입니다.');
    } else {
      setError(signInError.message);
    }
  } else {
    toast.success('로그인 되었습니다.');
    router.push('/');
  }
};

export const handleSignUp = async (
  email: string,
  password: string,
  nickname: string,
  router: any,
  setError: (message: string) => void
) => {
  const supabase = createClient();

  if (!email || !password || !nickname) {
    return toast.error('빈칸을 모두 채워주세요!');
  }

  if (!validateEmail(email)) {
    return toast.error('유효한 이메일 주소를 입력하세요!(꼭 .com 으로 끝나는 이메일이어야 합니다!)');
  }
  if (!validatePassword(password)) {
    return toast.error('비밀번호는 최소 8자 이상, 영문자, 숫자, 특수 문자를 포함해야 합니다!');
  }

  const { data: emailExist, error: emailError } = await supabase.from('users').select('id').eq('email', email).single();

  if (emailExist) {
    return toast.error('이미 사용 중인 이메일입니다!');
  }

  const { data: nicknameExist, error: nicknameError } = await supabase
    .from('users')
    .select('id')
    .eq('nickname', nickname)
    .single();

  if (nicknameExist) {
    return toast.error('이미 사용 중인 닉네임입니다!');
  }

  const { data, error } = await supabase.auth.signUp({
    email: email,
    password: password,
    options: {
      data: {
        nickname: nickname
      }
    }
  });

  if (error) {
    setError(error.message);
  } else {
    toast.success('회원가입 성공!');
    router.push('/');
  }
};

 

 

  • 역할: 로그인 및 회원가입 기능을 처리하는 서비스 함수들입니다.
  • 구성 요소:
    • createClient: Supabase 클라이언트를 생성하는 함수입니다.
    • validateEmail 및 validatePassword: 이메일과 비밀번호의 유효성을 검사하는 함수들입니다.
    • handleLogin: 사용자의 로그인 요청을 처리하는 함수입니다.
    • handleSignUp: 사용자의 회원가입 요청을 처리하는 함수입니다.
  • 설명: 이 파일은 사용자 인증을 위한 로직을 포함하고 있으며, 클라이언트 사이드 유효성 검사와 Supabase를 통한 인증 및 데이터베이스 조작을 수행합니다. 성공 또는 실패 시 적절한 메시지를 사용자에게 보여줍니다.

5. middleware.ts (Next.js 미들웨어 설정)

import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

 

 

  • 역할: Next.js 미들웨어를 설정하고, 요청 경로에 따라 세션을 업데이트합니다.
  • 구성 요소:
    • updateSession: 사용자 세션을 업데이트하는 함수입니다.
    • matcher: 특정 경로를 제외한 모든 요청에 대해 미들웨어를 적용하도록 설정합니다.
  • 설명: 이 파일은 Next.js의 미들웨어로 사용되어 모든 요청에 대해 세션을 업데이트하고, 특정 조건에 따라 리다이렉션을 처리합니다. 특정 파일 경로(정적 파일, 이미지 등)를 제외한 모든 요청에 대해 적용됩니다.

요약

  1. client.ts: 브라우저 클라이언트를 생성하여 브라우저 사이드에서 Supabase 서비스를 사용할 수 있도록 합니다.
  2. middleware.ts: 서버 클라이언트를 생성하고, 사용자 세션을 업데이트하는 미들웨어 함수로 사용됩니다.
  3. server.ts: 서버 클라이언트를 생성하여 서버 사이드에서 Supabase 서비스를 사용할 수 있도록 합니다.
  4. service.ts: 로그인 및 회원가입 기능을 처리하는 서비스 함수들을 포함하여 클라이언트 사이드 유효성 검사와 Supabase 인증을 처리합니다.
  5. src/middleware.ts: Next.js 미들웨어 설정 파일로, 모든 요청에 대해 세션을 업데이트하고 리다이렉션을 처리합니다.

 

api/reviews

GET , POST , PUT , DELETE 구현

import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';

export async function GET(req: NextRequest) {
  const supabase = createClient();
  const { data, error } = await supabase.from('reviews').select('*');

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json(data, { status: 200 });
}

export async function POST(req: NextRequest) {
  const supabase = createClient();
  const { content, rating, post_id, user_id } = await req.json();

  const { data, error } = await supabase.from('reviews').insert({ content, rating, post_id, user_id });

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json(data, { status: 201 });
}

export async function PUT(req: NextRequest) {
  const supabase = createClient();
  const { id, content, rating } = await req.json();

  const { data, error } = await supabase.from('reviews').update({ content, rating }).eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json(data, { status: 200 });
}

export async function DELETE(req: NextRequest) {
  const supabase = createClient();
  const { id } = await req.json();

  const { data, error } = await supabase.from('reviews').delete().eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json(data, { status: 200 });
}

 

 

(mypage)/my-page/page.tsx

마이 페이지 리뷰 관리 구현 ( 독자적인 테이블로 CRUD 동작 , 유저 및 게시글 구현 시 연결 예정 )

'use client';

import axios from 'axios';
import { useEffect, useState } from 'react';
import Link from 'next/link';

type Review = {
  id: string;
  created_at: string;
  post_id: string;
  user_id: string;
  content: string;
  rating: number;
};

const MyPage = () => {
  const [reviews, setReviews] = useState<Review[]>([]);

  useEffect(() => {
    const fetchReviews = async () => {
      const response = await axios.get('/api/mypage');
      setReviews(response.data);
    };

    fetchReviews();
  }, []);

  const handleDelete = async (id: string) => {
    await axios.delete('/api/mypage', { data: { id } });
    setReviews(reviews.filter((review) => review.id !== id));
  };

  return (
    <div>
      <h1>My Page</h1>
      <Link href={`/review-page`}>
        <button>Create New Review</button>
      </Link>
      {reviews.length === 0 ? (
        <div>Loading...</div>
      ) : (
        reviews.map((item) => (
          <div key={item.id}>
            <h2>{item.content}</h2>
            <p>Rating: {item.rating}</p>
            <button onClick={() => handleDelete(item.id)}>Delete</button>
            <Link href={`/review-page?id=${item.id}`}>
              <button>Edit</button>
            </Link>
          </div>
        ))
      )}
    </div>
  );
};

export default MyPage;

 

(mypage)/my-page/page.tsx

리뷰 작성 및 수정 페이지 ( 완료 및 취소 시 꼭 mypage가 아닌 이전페이지로 돌아갈 예정 )

'use client';

import axios from 'axios';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { v4 as uuidv4 } from 'uuid';

type Review = {
  id: string;
  created_at: string;
  post_id: string;
  user_id: string;
  content: string;
  rating: number;
};

const ReviewPage = () => {
  const [review, setReview] = useState<Review | null>(null);
  const [content, setContent] = useState('');
  const [rating, setRating] = useState(0);
  const router = useRouter();
  const searchParams = useSearchParams();
  const id = searchParams.get('id');

  useEffect(() => {
    const fetchReview = async () => {
      if (id) {
        const response = await axios.get(`/api/mypage?id=${id}`);
        const reviewData = response.data[0];
        setReview(reviewData);
        setContent(reviewData.content);
        setRating(reviewData.rating);
      }
    };

    fetchReview();
  }, [id]);

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    if (id) {
      await axios.put('/api/mypage', { id, content, rating });
    } else {
      const postId = uuidv4();
      const userId = uuidv4();
      await axios.post('/api/mypage', { content, rating, post_id: postId, user_id: userId });
    }
    router.back();
  };

  const handleDelete = async () => {
    if (id) {
      await axios.delete('/api/mypage', { data: { id } });
      router.back();
    }
  };

  const handleBack = () => {
    router.back();
  };

  return (
    <div>
      <h1>{id ? 'Edit Review' : 'New Review'}</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Content"
          className="text-black"
        />
        <input
          type="number"
          value={rating}
          onChange={(e) => setRating(parseInt(e.target.value))}
          placeholder="Rating"
          className="text-black"
        />
        <button type="submit">{id ? 'Update Review' : 'Add Review'}</button>
      </form>
      {id && <button onClick={handleDelete}>Delete Review</button>}
      <button onClick={handleBack}>Back</button>
    </div>
  );
};

export default ReviewPage;

 

회의 내용

튜터님 조언

  • 반응형 기획 시 웹보다는 앱을 먼저 기획하고 구현하는게 좋음
    • 작은집에서 큰집으로 이사가는 것을 생각하면됨
    • 반대로 할 시 기능적으로 문제가 생길 수 있고 시간도 오래 걸림

→ 앱 기준으로 가로 360px 작업 예정

회의

  • 결제 관련해서 외국인을 주 타겟층으로 고려하여 paypal API를 사용 예정
    • 연동 및 결제 대금에관하여 백오피스가 중앙에서 관리하게끔 기획 중
  • 컴포넌트 분리 방식 - app 폴더 아래 “components” 또는 app 폴더 아래 개인 page폴더 아래 “_components”
  • 폴더 분리 방식 - page.tsx 파일 안에 로직을 다 작성하기 또는 세부적으로 파일 나누기

노션 정리 , 피그마에 앱 시안 방향성 회의 , 각자 분담한 기능 구현 조사 및 시작