feat: SSAFY OAuth 2.0 로그인 클라이언트 구현
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:
2026-03-11 23:56:06 +09:00
parent 90e9922bde
commit c2e3be491d
5 changed files with 81 additions and 2 deletions

View File

@@ -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"

View File

@@ -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');

View File

@@ -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>
);

View File

@@ -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>

View 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>
);
}