Implement user authentication

Implement login functionality and user authentication logic.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-15 06:50:56 +00:00
parent bfb446fe1a
commit 33f1a94a81
8 changed files with 387 additions and 112 deletions

View File

@@ -19,6 +19,7 @@ import Login from "./pages/Login";
import Register from "./pages/Register"; import Register from "./pages/Register";
import ForgotPassword from "./pages/ForgotPassword"; import ForgotPassword from "./pages/ForgotPassword";
import { initSyncSettings } from "./utils/syncUtils"; import { initSyncSettings } from "./utils/syncUtils";
import { AuthProvider } from "./contexts/AuthContext";
// 전역 오류 처리 // 전역 오류 처리
const handleError = (error: any) => { const handleError = (error: any) => {
@@ -140,6 +141,7 @@ const App = () => {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<TooltipProvider> <TooltipProvider>
<AuthProvider>
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<BrowserRouter> <BrowserRouter>
@@ -159,6 +161,7 @@ const App = () => {
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</AuthProvider>
</TooltipProvider> </TooltipProvider>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -1,13 +1,20 @@
import React from 'react'; import React from 'react';
import { Bell } from 'lucide-react'; import { Bell } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
const Header: React.FC = () => { const Header: React.FC = () => {
const { user } = useAuth();
const userName = user?.user_metadata?.username || '익명';
return ( return (
<header className="py-8"> <header className="py-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-2xl font-bold neuro-text"></h1> <h1 className="text-2xl font-bold neuro-text">
{user ? `${userName}님, 반갑습니다` : '반갑습니다'}
</h1>
<p className="text-gray-500"> </p> <p className="text-gray-500"> </p>
</div> </div>
<button className="neuro-flat p-2.5 rounded-full"> <button className="neuro-flat p-2.5 rounded-full">

View File

@@ -4,28 +4,33 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { CloudUpload, RefreshCw } from "lucide-react"; import { CloudUpload, RefreshCw } from "lucide-react";
import { isSyncEnabled, setSyncEnabled, syncAllData, getLastSyncTime } from "@/utils/syncUtils"; import { isSyncEnabled, setSyncEnabled, syncAllData, getLastSyncTime } from "@/utils/syncUtils";
import { supabase } from "@/lib/supabase";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { useAuth } from "@/contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
const SyncSettings = () => { const SyncSettings = () => {
const [enabled, setEnabled] = useState(isSyncEnabled()); const [enabled, setEnabled] = useState(isSyncEnabled());
const [user, setUser] = useState<any>(null);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime()); const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime());
const { user } = useAuth();
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
// 사용자 정보 가져오기
const getUser = async () => {
const { data } = await supabase.auth.getUser();
setUser(data.user);
};
getUser();
// 마지막 동기화 시간 업데이트 // 마지막 동기화 시간 업데이트
setLastSync(getLastSyncTime()); setLastSync(getLastSyncTime());
}, []); }, []);
const handleSyncToggle = async (checked: boolean) => { const handleSyncToggle = async (checked: boolean) => {
if (!user && checked) {
toast({
title: "로그인 필요",
description: "데이터 동기화를 위해 로그인이 필요합니다.",
variant: "destructive"
});
return;
}
setEnabled(checked); setEnabled(checked);
setSyncEnabled(checked); setSyncEnabled(checked);
@@ -112,6 +117,7 @@ const SyncSettings = () => {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between items-center text-sm"> <div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground"> : {formatLastSyncTime()}</span> <span className="text-muted-foreground"> : {formatLastSyncTime()}</span>
{user ? (
<button <button
onClick={handleManualSync} onClick={handleManualSync}
disabled={syncing} disabled={syncing}
@@ -120,6 +126,15 @@ const SyncSettings = () => {
<RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
<span>{syncing ? '동기화 중...' : '지금 동기화'}</span> <span>{syncing ? '동기화 중...' : '지금 동기화'}</span>
</button> </button>
) : (
<Button
onClick={() => navigate('/login')}
size="sm"
className="py-1 px-3"
>
</Button>
)}
</div> </div>
</div> </div>
)} )}

View File

@@ -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<void>;
resetPassword: (email: string) => Promise<{ error: any }>;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth는 AuthProvider 내부에서 사용해야 합니다');
}
return context;
};

View File

@@ -1,54 +1,53 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowLeft, Mail, Check } from "lucide-react"; import { ArrowLeft, Mail, ArrowRight } from "lucide-react";
import { useToast } from "@/components/ui/use-toast"; import { useAuth } from "@/contexts/AuthContext";
const ForgotPassword = () => { const ForgotPassword = () => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false); const [isSent, setIsSent] = useState(false);
const { toast } = useToast(); const { resetPassword } = useAuth();
const handleSubmit = async (e: React.FormEvent) => { const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!email) { if (!email) {
toast({
title: "입력 오류",
description: "이메일을 입력해주세요.",
variant: "destructive",
});
return; return;
} }
setIsLoading(true); setIsLoading(true);
// 실제 비밀번호 찾기 로직은 추후 구현 try {
// 임시로 2초 후 성공으로 처리 const { error } = await resetPassword(email);
setTimeout(() => {
if (!error) {
setIsSent(true);
}
} finally {
setIsLoading(false); setIsLoading(false);
setIsSubmitted(true); }
toast({
title: "이메일 전송 완료",
description: "비밀번호 재설정 링크가 이메일로 전송되었습니다.",
});
}, 2000);
}; };
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background"> <div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold text-neuro-income mb-2"> </h1> <h1 className="text-3xl font-bold text-neuro-income mb-2"> </h1>
<p className="text-gray-500"> </p> <p className="text-gray-500">
{isSent
? "이메일을 확인하여 비밀번호를 재설정하세요"
: "가입한 이메일 주소를 입력하세요"}
</p>
</div> </div>
<div className="neuro-flat p-8 mb-6"> <div className="neuro-flat p-8 mb-6">
{!isSubmitted ? ( {!isSent ? (
<form onSubmit={handleSubmit}> <form onSubmit={handleResetPassword}>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email" className="text-base"></Label> <Label htmlFor="email" className="text-base"></Label>
@@ -59,7 +58,7 @@ const ForgotPassword = () => {
type="email" type="email"
placeholder="your@email.com" placeholder="your@email.com"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
className="pl-10 neuro-pressed" className="pl-10 neuro-pressed"
/> />
</div> </div>
@@ -67,36 +66,34 @@ const ForgotPassword = () => {
<Button <Button
type="submit" type="submit"
disabled={isLoading || !email}
className="w-full bg-neuro-income hover:bg-neuro-income/80 text-white py-6 h-auto" className="w-full bg-neuro-income hover:bg-neuro-income/80 text-white py-6 h-auto"
disabled={isLoading}
> >
{isLoading ? "처리 중..." : "재설정 링크 전송"} {isLoading ? "처리 중..." : "재설정 이메일 전송"} {!isLoading && <ArrowRight className="ml-2 h-5 w-5" />}
</Button> </Button>
</div> </div>
</form> </form>
) : ( ) : (
<div className="text-center space-y-6"> <div className="text-center py-4 space-y-6">
<div className="mx-auto w-16 h-16 neuro-flat rounded-full flex items-center justify-center text-neuro-income"> <p className="text-gray-700">
<Check className="h-8 w-8" /> <strong>{email}</strong> .
</div> </p>
<div> <p className="text-gray-500">
<h3 className="text-xl font-semibold mb-2"> </h3> . .
<p className="text-gray-500">
{email} . .
</p> </p>
</div>
<Button <Button
className="w-full bg-neuro-income hover:bg-neuro-income/80 text-white py-6 h-auto" onClick={() => setIsSent(false)}
onClick={() => setIsSubmitted(false)} variant="outline"
className="mt-4"
> >
</Button> </Button>
</div> </div>
)} )}
</div> </div>
<div className="text-center"> <div className="text-center">
<Link to="/login" className="inline-flex items-center text-neuro-income hover:underline"> <Link to="/login" className="text-neuro-income font-medium hover:underline inline-flex items-center">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
</Link> </Link>
</div> </div>

View File

@@ -1,21 +1,32 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, KeyRound, Eye, EyeOff } from "lucide-react"; import { ArrowRight, Mail, KeyRound, Eye, EyeOff } from "lucide-react";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { useAuth } from "@/contexts/AuthContext";
const Login = () => { const Login = () => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { const { toast } = useToast();
toast
} = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { signIn, user } = useAuth();
// 이미 로그인된 경우 메인 페이지로 리다이렉트
useEffect(() => {
if (user) {
navigate("/");
}
}, [user, navigate]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!email || !password) { if (!email || !password) {
toast({ toast({
title: "입력 오류", title: "입력 오류",
@@ -24,19 +35,20 @@ const Login = () => {
}); });
return; return;
} }
setIsLoading(true); setIsLoading(true);
// 실제 로그인 로직은 추후 구현 try {
// 임시로 2초 후 로그인 성공으로 처리 const { error } = await signIn(email, password);
setTimeout(() => {
setIsLoading(false); if (!error) {
toast({
title: "로그인 성공",
description: "환영합니다!"
});
navigate("/"); navigate("/");
}, 2000); }
} finally {
setIsLoading(false);
}
}; };
return <div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background"> return <div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
@@ -90,4 +102,5 @@ const Login = () => {
</div> </div>
</div>; </div>;
}; };
export default Login; export default Login;

View File

@@ -1,11 +1,12 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, KeyRound, User, Eye, EyeOff } from "lucide-react"; import { ArrowRight, Mail, KeyRound, User, Eye, EyeOff } from "lucide-react";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { useAuth } from "@/contexts/AuthContext";
const Register = () => { const Register = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -16,6 +17,14 @@ const Register = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { signUp, user } = useAuth();
// 이미 로그인된 경우 메인 페이지로 리다이렉트
useEffect(() => {
if (user) {
navigate("/");
}
}, [user, navigate]);
const handleRegister = async (e: React.FormEvent) => { const handleRegister = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -40,16 +49,15 @@ const Register = () => {
setIsLoading(true); setIsLoading(true);
// 실제 회원가입 로직은 추후 구현 try {
// 임시로 2초 후 회원가입 성공으로 처리 const { error, user } = await signUp(email, password, username);
setTimeout(() => {
setIsLoading(false); if (!error && user) {
toast({
title: "회원가입 성공",
description: "로그인 페이지로 이동합니다.",
});
navigate("/login"); navigate("/login");
}, 2000); }
} finally {
setIsLoading(false);
}
}; };
return ( return (

View File

@@ -5,6 +5,7 @@ import NavBar from '@/components/NavBar';
import SyncSettings from '@/components/SyncSettings'; import SyncSettings from '@/components/SyncSettings';
import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
const SettingsOption = ({ const SettingsOption = ({
icon: Icon, icon: Icon,
@@ -35,6 +36,13 @@ const SettingsOption = ({
const Settings = () => { const Settings = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, signOut } = useAuth();
const handleLogout = async () => {
await signOut();
navigate('/login');
};
return <div className="min-h-screen bg-neuro-background pb-24"> return <div className="min-h-screen bg-neuro-background pb-24">
<div className="max-w-md mx-auto px-6"> <div className="max-w-md mx-auto px-6">
{/* Header */} {/* Header */}
@@ -47,8 +55,12 @@ const Settings = () => {
<User size={24} /> <User size={24} />
</div> </div>
<div> <div>
<h2 className="font-semibold text-lg"></h2> <h2 className="font-semibold text-lg">
<p className="text-sm text-gray-500">honggildong@example.com</p> {user ? user.user_metadata?.username || '사용자' : '로그인 필요'}
</h2>
<p className="text-sm text-gray-500">
{user ? user.email : '계정에 로그인하세요'}
</p>
</div> </div>
</div> </div>
</header> </header>
@@ -74,7 +86,12 @@ const Settings = () => {
</div> </div>
<div className="mt-8"> <div className="mt-8">
<SettingsOption icon={LogOut} label="로그아웃" color="text-neuro-expense" /> <SettingsOption
icon={LogOut}
label={user ? "로그아웃" : "로그인"}
color="text-neuro-expense"
onClick={user ? handleLogout : () => navigate('/login')}
/>
</div> </div>
<div className="mt-12 text-center text-xs text-gray-400"> <div className="mt-12 text-center text-xs text-gray-400">