🎁 모든 강의 30% + 무료 강의 선물🎁

[인프런 워밍업 스터디 클럽 3기 풀스택] 3주차 발자국

[인프런 워밍업 스터디 클럽 3기 풀스택] 3주차 발자국



학습 내용

인프런 워밍업 클럽 스터디 3주차로,

이번 주는 넷플릭스 프로젝트를 다루는 시간이었다.

useInfiniteQueryJotai(recoil 대체 전역상태 라이브러리)를 사용해볼 수 있었다.



미션 3 구현 내용

과제 구현 저장소

image

Netflix 중 찜하기 관련 기능 

포인트 1: favorites 테이블 추가

image

actions/favoriteActions.ts

"use server";

import {
  createServerSupabaseClient,
  handleError,
  PostgrestError,
} from "@next-inflearn/supabase";

// Movie 타입 정의

export type Movie = {
  id: number;
  image_url: string;
  overview: string;
  popularity: number;
  release_date: string;
  title: string;
  vote_average: number;
  // Movie 타입에 favorites 필드 추가
  favorites?: {
    // optional field로 추가
    id: number;
  } | null;
};

// SearchMoviesResponse 타입 정의
export type SearchMoviesResponse = {
  data: Movie[];
  page: number;
  pageSize: number;
  hasNextPage: boolean;
};

// 에러 케이스를 위한 타입 정의
type SearchMoviesError = {
  data: never[];
  count: number;
  page: null;
  pageSize: null;
  error: PostgrestError;
};

// 성공 케이스를 위한 타입 정의
type SearchMoviesSuccess = {
  data: Movie[];
  page: number;
  pageSize: number;
  hasNextPage: boolean;
};

export async function searchMovies({
  search,
  page,
  pageSize,
}: {
  search: string;
  page: number;
  pageSize: number;
}): Promise<SearchMoviesSuccess> {
  const supabase = await createServerSupabaseClient();

  // 현재 사용자 정보 가져오기
  const {
    data: { user },
  } = await supabase.auth.getUser();

  // 쿼리 설정
  const query = supabase
    .from("movie")
    .select(
      user
        ? `
        *,
        favorites!left (
          id
        )
      `
        : "*", // 로그인하지 않은 경우 favorites 정보를 가져오지 않음
      { count: "exact" }
    )
    .ilike("title", `%${search}%`)
    .order("popularity", { ascending: false });

  // 로그인한 경우 현재 사용자의 즐겨찾기만 조회하도록 필터링
  if (user) {
    query.eq("favorites.user_id", user.id);
  }

  const { data, count, error } = await query.range(
    (page - 1) * pageSize,
    page * pageSize - 1
  );

  const hasNextPage = count ? count > page * pageSize : false;

  if (error) {
    return {
      data: [],
      page,
      pageSize,
      hasNextPage: false,
    };
  }

  // 반환된 데이터를 Movie 타입에 맞게 변환
  const moviesWithFavorites = (data || []).map((movie: any) => ({
    ...movie,
    favorites: movie.favorites?.[0] || null, // 즐겨찾기 정보 포함
  }));

  return {
    data: moviesWithFavorites,
    page,
    pageSize,
    hasNextPage,
  };
}

export async function getMovie(id: string) {
  const supabase = await createServerSupabaseClient();

  const { data, error } = await supabase
    .from("movie")
    .select("*")
    .eq("id", id)
    .maybeSingle();

  handleError(error);

  return data;
}

포인트 2: profiles 테이블 추가

imageutils/AuthProvider.tsx

"use client";

import { useEffect } from "react";
import { createBrowserSupabaseClient } from "@next-inflearn/supabase";
import { useSetAtom } from "jotai";
import { userAtom } from "@/utils/jotai/atoms";

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const setUser = useSetAtom(userAtom);
  const supabase = createBrowserSupabaseClient();

  useEffect(() => {
    // 현재 세션 확인
    supabase.auth.getSession().then(({ data: { session } }) => {
      setUser(session?.user ?? null);
    });

    // Auth 상태 변경 구독
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setUser(session?.user ?? null);
    });

    return () => subscription.unsubscribe();
  }, [supabase, setUser]);

  return children;
}

포인트 3: movies / favorites / profiles 테이블 연결

favorites 테이블 내

imageprofiles 테이블 내

image


회고

 

시간이 부족하다는 핑계로,

더 디벨롭할 수 있는 부분이 많음에도 불구하고 생각했던 것들을 다 구현하진 못했던 것 같다.

다만, 이렇게 틀을 갖춰놧으니 추후에 추가적인 기능을 정의해서 구현해보기 너무 좋을 것 같다.

 

 

댓글을 작성해보세요.


채널톡 아이콘