feat: SSAFY OAuth 2.0 로그인 클라이언트 구현
All checks were successful
Client CI/CD / deploy (push) Successful in 34s
All checks were successful
Client CI/CD / deploy (push) Successful in 34s
SSAFY 로그인 버튼 연동, 콜백 페이지 추가, AuthContext에 setUserFromSSAFY 메서드 추가. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import LoginPage from './pages/LoginPage';
|
||||
import RegisterPage from './pages/RegisterPage';
|
||||
import HomePage from './pages/HomePage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import SSAFYCallbackPage from './pages/SSAFYCallbackPage';
|
||||
|
||||
function AuthRedirect() {
|
||||
const { user } = useAuth();
|
||||
@@ -42,6 +43,7 @@ function AppRoutes() {
|
||||
<Routes>
|
||||
<Route path="/login" element={user ? <Navigate to="/" replace /> : <LoginPage />} />
|
||||
<Route path="/register" element={user ? <Navigate to="/" replace /> : <RegisterPage />} />
|
||||
<Route path="/auth/ssafy/callback" element={<SSAFYCallbackPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route
|
||||
path="/admin"
|
||||
|
||||
@@ -18,6 +18,17 @@ export async function logout() {
|
||||
return apiFetch('/api/auth/logout', { method: 'POST' });
|
||||
}
|
||||
|
||||
export async function getSSAFYLoginURL() {
|
||||
return apiFetch('/api/auth/ssafy/login');
|
||||
}
|
||||
|
||||
export async function ssafyCallback(code) {
|
||||
return apiFetch('/api/auth/ssafy/callback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
}
|
||||
|
||||
// 토큰을 리프레시하고 새 access token을 반환
|
||||
export async function refreshToken() {
|
||||
const rt = localStorage.getItem('refreshToken');
|
||||
|
||||
@@ -19,6 +19,15 @@ export function AuthProvider({ children }) {
|
||||
setUser({ token: data.token, username: data.username, role: data.role });
|
||||
}, []);
|
||||
|
||||
// SSAFY OAuth 콜백에서 받은 토큰으로 로그인 처리
|
||||
const setUserFromSSAFY = useCallback((data) => {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('refreshToken', data.refreshToken);
|
||||
localStorage.setItem('username', data.username);
|
||||
localStorage.setItem('role', data.role);
|
||||
setUser({ token: data.token, username: data.username, role: data.role });
|
||||
}, []);
|
||||
|
||||
// 로컬 세션만 정리 (토큰 만료·강제 로그아웃 시)
|
||||
const clearSession = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
@@ -43,7 +52,7 @@ export function AuthProvider({ children }) {
|
||||
}, [clearSession]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, login, logout }}>
|
||||
<AuthContext.Provider value={{ user, login, logout, setUserFromSSAFY }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
import { getSSAFYLoginURL } from '../api/auth';
|
||||
import './LoginPage.css';
|
||||
|
||||
export default function LoginPage() {
|
||||
@@ -75,7 +76,14 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn-ssafy"
|
||||
onClick={() => alert('SSAFY 로그인은 준비 중입니다.')}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const data = await getSSAFYLoginURL();
|
||||
window.location.href = data.url;
|
||||
} catch {
|
||||
setError('SSAFY 로그인 URL을 가져올 수 없습니다.');
|
||||
}
|
||||
}}
|
||||
>
|
||||
SSAFY 계정으로 로그인
|
||||
</button>
|
||||
|
||||
49
src/pages/SSAFYCallbackPage.jsx
Normal file
49
src/pages/SSAFYCallbackPage.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
import { ssafyCallback } from '../api/auth';
|
||||
|
||||
export default function SSAFYCallbackPage() {
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { setUserFromSSAFY } = useAuth();
|
||||
const called = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (called.current) return;
|
||||
called.current = true;
|
||||
|
||||
const code = searchParams.get('code');
|
||||
if (!code) {
|
||||
setError('인가 코드가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
ssafyCallback(code)
|
||||
.then((data) => {
|
||||
setUserFromSSAFY(data);
|
||||
navigate('/', { replace: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message || 'SSAFY 로그인에 실패했습니다.');
|
||||
});
|
||||
}, [searchParams, setUserFromSSAFY, navigate]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginTop: '4rem' }}>
|
||||
<p style={{ color: '#e74c3c' }}>{error}</p>
|
||||
<button onClick={() => navigate('/login', { replace: true })}>
|
||||
로그인 페이지로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginTop: '4rem' }}>
|
||||
<p>SSAFY 로그인 처리 중...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user