From 33f1a94a812043a4295f44bb28ca8a77edd10974 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 06:50:56 +0000 Subject: [PATCH] Implement user authentication Implement login functionality and user authentication logic. --- src/App.tsx | 41 +++--- src/components/Header.tsx | 9 +- src/components/SyncSettings.tsx | 49 +++++--- src/contexts/AuthContext.tsx | 215 ++++++++++++++++++++++++++++++++ src/pages/ForgotPassword.tsx | 95 +++++++------- src/pages/Login.tsx | 39 ++++-- src/pages/Register.tsx | 28 +++-- src/pages/Settings.tsx | 23 +++- 8 files changed, 387 insertions(+), 112 deletions(-) create mode 100644 src/contexts/AuthContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 272fb3e..ceab610 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import Login from "./pages/Login"; import Register from "./pages/Register"; import ForgotPassword from "./pages/ForgotPassword"; import { initSyncSettings } from "./utils/syncUtils"; +import { AuthProvider } from "./contexts/AuthContext"; // 전역 오류 처리 const handleError = (error: any) => { @@ -140,25 +141,27 @@ const App = () => { return ( - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8d51191..06371f5 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,13 +1,20 @@ import React from 'react'; import { Bell } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; const Header: React.FC = () => { + const { user } = useAuth(); + + const userName = user?.user_metadata?.username || '익명'; + return (
-

반갑습니다

+

+ {user ? `${userName}님, 반갑습니다` : '반갑습니다'} +

젤리의 적자탈출

+ {user ? ( + + ) : ( + + )}
)} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..1889106 --- /dev/null +++ b/src/contexts/AuthContext.tsx @@ -0,0 +1,215 @@ + +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { supabase } from '@/lib/supabase'; +import { Session, User } from '@supabase/supabase-js'; +import { useToast } from '@/components/ui/use-toast'; + +type AuthContextType = { + session: Session | null; + user: User | null; + loading: boolean; + signIn: (email: string, password: string) => Promise<{ error: any }>; + signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; + signOut: () => Promise; + resetPassword: (email: string) => Promise<{ error: any }>; +}; + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [session, setSession] = useState(null); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + + useEffect(() => { + // 현재 세션 체크 + const getSession = async () => { + try { + const { data, error } = await supabase.auth.getSession(); + + if (error) { + console.error('세션 로딩 중 오류:', error); + toast({ + title: '세션 로드 오류', + description: '사용자 세션을 불러오는 중 문제가 발생했습니다.', + variant: 'destructive', + }); + } else { + setSession(data.session); + setUser(data.session?.user ?? null); + } + } catch (error) { + console.error('세션 확인 중 예외 발생:', error); + } finally { + setLoading(false); + } + }; + + getSession(); + + // auth 상태 변경 리스너 + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + console.log('Supabase auth 이벤트:', event); + setSession(session); + setUser(session?.user ?? null); + setLoading(false); + } + ); + + // 리스너 정리 + return () => { + subscription.unsubscribe(); + }; + }, [toast]); + + const signIn = async (email: string, password: string) => { + try { + const { error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) { + console.error('로그인 오류:', error); + toast({ + title: '로그인 실패', + description: error.message, + variant: 'destructive', + }); + return { error }; + } + + toast({ + title: '로그인 성공', + description: '환영합니다!', + }); + return { error: null }; + } catch (error: any) { + console.error('로그인 중 예외 발생:', error); + toast({ + title: '로그인 오류', + description: '예상치 못한 오류가 발생했습니다.', + variant: 'destructive', + }); + return { error }; + } + }; + + const signUp = async (email: string, password: string, username: string) => { + try { + // Supabase 인증으로 사용자 생성 + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + username, + }, + }, + }); + + if (error) { + console.error('회원가입 오류:', error); + toast({ + title: '회원가입 실패', + description: error.message, + variant: 'destructive', + }); + return { error, user: null }; + } + + toast({ + title: '회원가입 성공', + description: '이메일 확인 후 로그인해주세요.', + }); + + return { error: null, user: data.user }; + } catch (error: any) { + console.error('회원가입 중 예외 발생:', error); + toast({ + title: '회원가입 오류', + description: '예상치 못한 오류가 발생했습니다.', + variant: 'destructive', + }); + return { error, user: null }; + } + }; + + const signOut = async () => { + try { + const { error } = await supabase.auth.signOut(); + + if (error) { + console.error('로그아웃 오류:', error); + toast({ + title: '로그아웃 실패', + description: error.message, + variant: 'destructive', + }); + return; + } + + toast({ + title: '로그아웃 성공', + description: '다음에 또 만나요!', + }); + } catch (error) { + console.error('로그아웃 중 예외 발생:', error); + toast({ + title: '로그아웃 오류', + description: '예상치 못한 오류가 발생했습니다.', + variant: 'destructive', + }); + } + }; + + const resetPassword = async (email: string) => { + try { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: window.location.origin + '/reset-password', + }); + + if (error) { + console.error('비밀번호 재설정 오류:', error); + toast({ + title: '비밀번호 재설정 실패', + description: error.message, + variant: 'destructive', + }); + return { error }; + } + + toast({ + title: '비밀번호 재설정 이메일 전송됨', + description: '이메일을 확인하여 비밀번호를 재설정해주세요.', + }); + return { error: null }; + } catch (error: any) { + console.error('비밀번호 재설정 중 예외 발생:', error); + toast({ + title: '비밀번호 재설정 오류', + description: '예상치 못한 오류가 발생했습니다.', + variant: 'destructive', + }); + return { error }; + } + }; + + const value = { + session, + user, + loading, + signIn, + signUp, + signOut, + resetPassword, + }; + + return {children}; +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth는 AuthProvider 내부에서 사용해야 합니다'); + } + return context; +}; diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx index e1f54de..61d1306 100644 --- a/src/pages/ForgotPassword.tsx +++ b/src/pages/ForgotPassword.tsx @@ -1,102 +1,99 @@ + import React, { useState } from "react"; import { Link } from "react-router-dom"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Mail, Check } from "lucide-react"; -import { useToast } from "@/components/ui/use-toast"; +import { ArrowLeft, Mail, ArrowRight } from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; const ForgotPassword = () => { const [email, setEmail] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [isSubmitted, setIsSubmitted] = useState(false); - const { toast } = useToast(); + const [isSent, setIsSent] = useState(false); + const { resetPassword } = useAuth(); - const handleSubmit = async (e: React.FormEvent) => { + const handleResetPassword = async (e: React.FormEvent) => { e.preventDefault(); if (!email) { - toast({ - title: "입력 오류", - description: "이메일을 입력해주세요.", - variant: "destructive", - }); return; } setIsLoading(true); - // 실제 비밀번호 찾기 로직은 추후 구현 - // 임시로 2초 후 성공으로 처리 - setTimeout(() => { + try { + const { error } = await resetPassword(email); + + if (!error) { + setIsSent(true); + } + } finally { setIsLoading(false); - setIsSubmitted(true); - toast({ - title: "이메일 전송 완료", - description: "비밀번호 재설정 링크가 이메일로 전송되었습니다.", - }); - }, 2000); + } }; return (
-

비밀번호 찾기

-

젤리의 적자탈출 계정에 등록된 이메일을 입력해주세요

+

비밀번호 재설정

+

+ {isSent + ? "이메일을 확인하여 비밀번호를 재설정하세요" + : "가입한 이메일 주소를 입력하세요"} +

- {!isSubmitted ? ( -
+ {!isSent ? ( +
- setEmail(e.target.value)} - className="pl-10 neuro-pressed" + setEmail(e.target.value)} + className="pl-10 neuro-pressed" />
-
) : ( -
-
- -
-
-

이메일이 전송되었습니다

-

- {email}로 비밀번호 재설정 링크를 보냈습니다. 이메일을 확인해주세요. -

-
-
)}
- + 로그인으로 돌아가기
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 1f88c36..ae3332a 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,21 +1,32 @@ -import React, { useState } from "react"; + +import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ArrowRight, Mail, KeyRound, Eye, EyeOff } from "lucide-react"; import { useToast } from "@/components/ui/use-toast"; +import { useAuth } from "@/contexts/AuthContext"; + const Login = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { - toast - } = useToast(); + const { toast } = useToast(); const navigate = useNavigate(); + const { signIn, user } = useAuth(); + + // 이미 로그인된 경우 메인 페이지로 리다이렉트 + useEffect(() => { + if (user) { + navigate("/"); + } + }, [user, navigate]); + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); + if (!email || !password) { toast({ title: "입력 오류", @@ -24,19 +35,20 @@ const Login = () => { }); return; } + setIsLoading(true); - // 실제 로그인 로직은 추후 구현 - // 임시로 2초 후 로그인 성공으로 처리 - setTimeout(() => { + try { + const { error } = await signIn(email, password); + + if (!error) { + navigate("/"); + } + } finally { setIsLoading(false); - toast({ - title: "로그인 성공", - description: "환영합니다!" - }); - navigate("/"); - }, 2000); + } }; + return
@@ -90,4 +102,5 @@ const Login = () => {
; }; + export default Login; diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 9d363b6..f090762 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,11 +1,12 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ArrowRight, Mail, KeyRound, User, Eye, EyeOff } from "lucide-react"; import { useToast } from "@/components/ui/use-toast"; +import { useAuth } from "@/contexts/AuthContext"; const Register = () => { const [username, setUsername] = useState(""); @@ -16,6 +17,14 @@ const Register = () => { const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); const navigate = useNavigate(); + const { signUp, user } = useAuth(); + + // 이미 로그인된 경우 메인 페이지로 리다이렉트 + useEffect(() => { + if (user) { + navigate("/"); + } + }, [user, navigate]); const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); @@ -40,16 +49,15 @@ const Register = () => { setIsLoading(true); - // 실제 회원가입 로직은 추후 구현 - // 임시로 2초 후 회원가입 성공으로 처리 - setTimeout(() => { + try { + const { error, user } = await signUp(email, password, username); + + if (!error && user) { + navigate("/login"); + } + } finally { setIsLoading(false); - toast({ - title: "회원가입 성공", - description: "로그인 페이지로 이동합니다.", - }); - navigate("/login"); - }, 2000); + } }; return ( diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 1955b91..24e608a 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -5,6 +5,7 @@ import NavBar from '@/components/NavBar'; import SyncSettings from '@/components/SyncSettings'; import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { useAuth } from '@/contexts/AuthContext'; const SettingsOption = ({ icon: Icon, @@ -35,6 +36,13 @@ const SettingsOption = ({ const Settings = () => { const navigate = useNavigate(); + const { user, signOut } = useAuth(); + + const handleLogout = async () => { + await signOut(); + navigate('/login'); + }; + return
{/* Header */} @@ -47,8 +55,12 @@ const Settings = () => {
-

홍길동

-

honggildong@example.com

+

+ {user ? user.user_metadata?.username || '사용자' : '로그인 필요'} +

+

+ {user ? user.email : '계정에 로그인하세요'} +

@@ -74,7 +86,12 @@ const Settings = () => {
- + navigate('/login')} + />