Implement user authentication
Implement login functionality and user authentication logic.
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
215
src/contexts/AuthContext.tsx
Normal file
215
src/contexts/AuthContext.tsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user