inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

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

hee j
0

목차


 

1. 회원가입 & 로그인

[Supabase Auth 소개]

[인증 구현 - Confirmation URL 방식]

image

// 가입하기 완료 확인 체크
const [confirmaitonRequired, setConfirmationRequired] = useState(false);
const supabase = createBrowserSupabaseClient();

const signupMutation = useMutation({
    mutationFn: async () => {
      const { data, error } = await supabase.auth.signUp({
        email,
        password,
        options: {
          // 유저가 회원가입 끝나고 이메일 링크 클릭 => 서버처리 완료 => 해당 url로 다시 이동
          emailRedirectTo: "http://loacalhost:3000/signup/confirm",
        },
      }); 
      if (data) {
        setConfirmationRequired(true);
      }
      if (error) {
        alert(error.message);
      }
    },
  });
export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    const supabase = await createServerSupabaseClient();
    await supabase.auth.exchangeCodeForSession(code); // 세션 획득
  }

  return NextResponse.redirect(requestUrl.origin); // 화면 메인페이지로 이동
}

 

[인증 구현 - 6-digit OTP 방식]

  const verityOtpMutation = useMutation({
    mutationFn: async () => {
      const { data, error } = await supabase.auth.verifyOtp({
        type: "signup",
        email,
        token: otp,
      });
      if (data) {
        setConfirmationRequired(true);
      }
      if (error) {
        alert(error.message);
      }
    },
  });

 

2. 채팅 기능 구현

 [Supabse Realtime 소개]

[실시간 채팅 구현]

export async function sendMessage({ message, chatUserId }) {
  const supabase = await createServerSupabaseClient(); // 현재 로그인한 유저

  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error || !session.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: sendMessageError } = await supabase
    .from("message")
    .insert({
      message,
      receiver: chatUserId,
      sender: session.user.id,
    });

  if (sendMessageError) {
    throw new Error(sendMessageError.message);
  }

  return data;
}
export async function getAllMessages({ chatUserId }) {
  // 현재 나와 채팅중인 메시지 모두 가져오기
  const supabase = await createServerSupabaseClient();
  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error || !session.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: getMessagesError } = await supabase
    .from("message")
    .select("*")
    .or(`receiver.eq.${chatUserId},receiver.eq.${session.user.id}`) // 상대방 또는 나
    .or(`sender.eq.${chatUserId},sender.eq.${session.user.id}`) 
    .order("created_at", { ascending: true });

  if (getMessagesError) {
    return [];
  }

  return data;
}
const sendMessageMutation = useMutation({
    mutationFn: async () => {
      return sendMessage({
        message,
        chatUserId: selectedUserId,
      });
    },
    onSuccess: () => {
      setMessage(""); // 메시지 입력 칸 비우기
      getAllMessagesQuery.refetch(); // 메시지 가져오기
    },
  });

  const getAllMessagesQuery = useQuery({
    queryKey: ["messages", selectedUserId],
    queryFn: () => getAllMessages({ chatUserId: selectedUserId }),
  });

  // 새로고침 없이 대화를 실시간으로 표시
  useEffect(() => {
    const channel = supabase
      .channel("message_postgres_changes")
      .on(
        "postgres_changes",
        {
          event: "INSERT",
          schema: "public",
          table: "message",
        },
        (payload) => {
          if (payload.eventType === "INSERT" && !payload.errors) {
            getAllMessagesQuery.refetch();
          }
        }
      )
      .subscribe();

    return () => {
      channel.unsubscribe();
    };
  }, []);

 

[유저별 마지막 접속 시간 구현]

  const supabase = createBrowserSupabaseClient();

  useEffect(() => {
    const channel = supabase.channel("online_users", {
      config: {
        presence: {
          key: loggedInUser.id,
        },
      },
    });

    channel.on("presence", { event: "sync" }, () => {
      const newState = channel.presenceState();
      console.log(newState);
    });

    channel.subscribe(async (status) => {
       // 구독이 완료되지 않았을 때 종료
      if (status !== "SUBSCRIBED") {
        return;
      }
      // 구독 성공시 새로운 presence status가 들어옴
      // channel.track안에 트래킹하고 싶은 오브젝트 넣기
      const newPresenceStatus = await channel.track({
        onlineAt: new Date().toISOString(),
      });
    });

    return () => {
      channel.unsubscribe();
    };
  }, []);
export const presenceState = atom({
  key: "presenceState",
  default: null,
});
  const [presence, setPresence] = useRecoilState(presenceState);
 ...
    channel.on("presence", { event: "sync" }, () => {
      const newState = channel.presenceState();
      // newState를 그냥 쓰면 값을 마음대로 바꿔버릴 수 있기 때문에
      // 다시 파싱하여 오브젝트를 깔끔하게 셋팅해줌
      const newStateObj = JSON.parse(JSON.stringify(newState));
      setPresence(newStateObj);
    });
...
 return (
    <div className="h-screen min-w-60 flex flex-col bg-gray-50">
      {getAllUsersQuery.data?.map((user, index) => (
        <Person
          onClick={() => {
            setSelectedUserId(user.id);
            setSelectedUserIndex(index);
          }}
          index={index}
          isActive={selectedUserId === user.id}
          name={user.email.split("@")[0]}
          onChatScreen={false}
          onlineAt={presence?.[user.id]?.[0]?.onlineAt}
          // onlineAt={new Date().toISOString()}
          userId={user.id}
        />
      ))}

 

 3. 4주차 제출

 [메시지 삭제]

export async function updateMessage({ messageId }: { messageId: string }) {
  const supabase = await createBrowserSupabaseClient();

  const {
    data: { session },
    error: authError,
  } = await supabase.auth.getSession();

  if (authError || !session?.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: updateError } = await supabase
    .from("message")
    .update({ message: "이 메시지는 삭제되었습니다", is_deleted: true }) // 메시지 내용 변경
    .eq("id", messageId);

  if (updateError) {
    throw new Error(updateError.message);
  }

  return data;
}

 

 

 

 

답변 0