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 RegisterPage from './pages/RegisterPage';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import AdminPage from './pages/AdminPage';
|
import AdminPage from './pages/AdminPage';
|
||||||
|
import SSAFYCallbackPage from './pages/SSAFYCallbackPage';
|
||||||
|
|
||||||
function AuthRedirect() {
|
function AuthRedirect() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -42,6 +43,7 @@ function AppRoutes() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={user ? <Navigate to="/" replace /> : <LoginPage />} />
|
<Route path="/login" element={user ? <Navigate to="/" replace /> : <LoginPage />} />
|
||||||
<Route path="/register" element={user ? <Navigate to="/" replace /> : <RegisterPage />} />
|
<Route path="/register" element={user ? <Navigate to="/" replace /> : <RegisterPage />} />
|
||||||
|
<Route path="/auth/ssafy/callback" element={<SSAFYCallbackPage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ export async function logout() {
|
|||||||
return apiFetch('/api/auth/logout', { method: 'POST' });
|
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을 반환
|
// 토큰을 리프레시하고 새 access token을 반환
|
||||||
export async function refreshToken() {
|
export async function refreshToken() {
|
||||||
const rt = localStorage.getItem('refreshToken');
|
const rt = localStorage.getItem('refreshToken');
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ export function AuthProvider({ children }) {
|
|||||||
setUser({ token: data.token, username: data.username, role: data.role });
|
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(() => {
|
const clearSession = useCallback(() => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
@@ -43,7 +52,7 @@ export function AuthProvider({ children }) {
|
|||||||
}, [clearSession]);
|
}, [clearSession]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, login, logout }}>
|
<AuthContext.Provider value={{ user, login, logout, setUserFromSSAFY }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, Link, useLocation } from 'react-router-dom';
|
import { useNavigate, Link, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
import { getSSAFYLoginURL } from '../api/auth';
|
||||||
import './LoginPage.css';
|
import './LoginPage.css';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
@@ -75,7 +76,14 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-ssafy"
|
className="btn-ssafy"
|
||||||
onClick={() => alert('SSAFY 로그인은 준비 중입니다.')}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const data = await getSSAFYLoginURL();
|
||||||
|
window.location.href = data.url;
|
||||||
|
} catch {
|
||||||
|
setError('SSAFY 로그인 URL을 가져올 수 없습니다.');
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
SSAFY 계정으로 로그인
|
SSAFY 계정으로 로그인
|
||||||
</button>
|
</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