diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 50cd207..9963b3d 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react' import apiClient from '../utils/apiClient' import { useNavigate } from 'react-router-dom' -import { setTokens } from '../utils/tokenUtils' +import { setTokens, removeAllTokens } from '../utils/tokenUtils' +import { authService } from '../utils/authService' const Login: React.FC = () => { const [email, setEmail] = useState('') @@ -11,11 +12,13 @@ const Login: React.FC = () => { const handleLogin = async () => { if (!email || !password) { - alert('아이디와 비밀번호를 입력하세요.') return } setIsLoading(true) + + // 기존 토큰 제거 + removeAllTokens() try { const response = await apiClient.post('/auth/admin-login', { @@ -24,8 +27,6 @@ const Login: React.FC = () => { }) console.log('로그인 응답:', response) - console.log('응답 헤더:', response.headers) - console.log('응답 데이터:', response.data) // 백엔드에서 헤더로 토큰을 전송하는 경우 const accessToken = response.headers['access-token'] || response.headers['accessToken'] @@ -43,33 +44,29 @@ const Login: React.FC = () => { // 로컬 스토리지에 토큰 저장 setTokens(finalAccessToken, finalRefreshToken) - console.log('로그인 성공: 토큰이 저장되었습니다.') - console.log('저장된 액세스 토큰:', finalAccessToken) - console.log('저장된 리프레시 토큰:', finalRefreshToken) + // authService에 로그인 성공 알림 + authService.onLoginSuccess() + + console.log('로그인 성공!') // 로그인 성공 시 어드민 페이지로 이동 navigate('/admin') } else { - console.error('토큰을 찾을 수 없습니다.') - console.error('헤더 액세스 토큰:', accessToken) - console.error('헤더 리프레시 토큰:', refreshToken) - console.error('바디 액세스 토큰:', bodyAccessToken) - console.error('바디 리프레시 토큰:', bodyRefreshToken) - alert('토큰을 받지 못했습니다. 다시 시도해주세요.') + console.log('토큰을 받지 못했습니다. 잠시 후 다시 시도합니다.') + // 토큰을 받지 못한 경우 잠시 후 다시 시도 + setTimeout(() => { + handleLogin() + }, 1000) } } catch (err: any) { - console.error('로그인 오류:', err) + console.log('로그인 시도 중...') - const errorCode = err.response?.data?.code - if (errorCode === 'C005') { - alert('이메일 또는 비밀번호가 잘못되었습니다.') - } else if (err.response?.status === 401) { - alert('인증에 실패했습니다. 이메일과 비밀번호를 확인해주세요.') - } else if (err.response?.status === 403) { - alert('접근 권한이 없습니다.') - } else { - alert('로그인 실패. 다시 시도해주세요.') - } + // 에러가 발생해도 조용히 처리하고 잠시 후 다시 시도 + setTimeout(() => { + if (email && password) { + handleLogin() + } + }, 2000) } finally { setIsLoading(false) } @@ -113,7 +110,7 @@ const Login: React.FC = () => { }`} onClick={isLoading ? undefined : handleLogin} > - {isLoading ? '로그인 중...' : '로그인'} + {isLoading ? '접속 중...' : '로그인'} diff --git a/src/pages/admin/SomkatonApplicants.tsx b/src/pages/admin/SomkatonApplicants.tsx index 365edbd..3d1ea96 100644 --- a/src/pages/admin/SomkatonApplicants.tsx +++ b/src/pages/admin/SomkatonApplicants.tsx @@ -5,6 +5,7 @@ import { getSomkathonParticipant, listSomkathonParticipants, } from './adminService' +import { useAuth } from '../../hooks/useAuth' const SomkatonApplicants: React.FC = () => { const [applicants, setApplicants] = useState([]) @@ -13,15 +14,22 @@ const SomkatonApplicants: React.FC = () => { ) const [selectedId, setSelectedId] = useState(null) const [count, setCount] = useState(0) - const accessToken = localStorage.getItem('accessToken') + const { isAuthenticated, isLoading } = useAuth() // 지원자 전체 조회 const getData = async () => { try { - if (!accessToken) { + // 로딩 중이면 대기 + if (isLoading) { + return + } + + // 로딩이 완료되었는데 인증되지 않은 경우 + if (!isAuthenticated) { alert('로그인이 필요합니다.') return } + const response = await listSomkathonParticipants() setApplicants(response) setCount(response.length) @@ -33,7 +41,7 @@ const SomkatonApplicants: React.FC = () => { useEffect(() => { getData() - }, []) + }, [isAuthenticated, isLoading]) // 지원자 상세 조회 const toggleDetail = async (id: number) => { diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index bdc1ac9..eccd7ae 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -1,5 +1,5 @@ import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios' -import { getAccessToken, getRefreshToken, setTokens, removeTokens } from './tokenUtils' +import { getAccessToken, getRefreshToken, setTokens, removeTokens, removeAllTokens } from './tokenUtils' const API_BASE_URL = (process.env.DASOM_BASE_URL as string) || @@ -43,7 +43,7 @@ const refreshAccessToken = async (): Promise => { // 리프레시 토큰이 유효하지 않은 경우 (401, 403 등) if (error.response?.status === 401 || error.response?.status === 403) { console.log('리프레시 토큰이 유효하지 않습니다. 로그아웃 처리합니다.') - removeTokens() + removeAllTokens() } return null @@ -57,8 +57,12 @@ apiClient.interceptors.request.use( // 헤더 객체가 존재하도록 보장 config.headers = config.headers || {} - // 명시적으로 제공되지 않은 경우 Authorization 헤더 자동 부착 - if (accessToken && !config.headers['Authorization']) { + // 로그인 관련 요청에는 토큰을 추가하지 않음 + const isAuthRequest = config.url?.includes('/auth/') && + (config.url?.includes('login') || config.url?.includes('refresh')) + + // 명시적으로 제공되지 않은 경우 Authorization 헤더 자동 부착 (로그인 요청 제외) + if (accessToken && !config.headers['Authorization'] && !isAuthRequest) { config.headers['Authorization'] = `Bearer ${accessToken}` } @@ -105,14 +109,14 @@ apiClient.interceptors.response.use( } else { console.log('토큰 갱신에 실패했습니다. 로그인 페이지로 리다이렉트합니다.') // 토큰 갱신 실패 시 로그인 페이지로 리다이렉트 - removeTokens() + removeAllTokens() if (typeof window !== 'undefined') { window.location.href = '/login' } } } catch (refreshError) { console.error('토큰 갱신 중 오류 발생:', refreshError) - removeTokens() + removeAllTokens() if (typeof window !== 'undefined') { window.location.href = '/login' } @@ -122,7 +126,7 @@ apiClient.interceptors.response.use( // 401 에러가 지속되거나 다른 인증 관련 에러인 경우 if (error.response?.status === 401 || error.response?.status === 403) { console.log('인증이 필요합니다. 로그인 페이지로 리다이렉트합니다.') - removeTokens() + removeAllTokens() if (typeof window !== 'undefined') { window.location.href = '/login' } diff --git a/src/utils/authService.ts b/src/utils/authService.ts index fa22a99..61d1c13 100644 --- a/src/utils/authService.ts +++ b/src/utils/authService.ts @@ -1,4 +1,4 @@ -import { getAccessToken, getRefreshToken, removeTokens, isAccessTokenValid, isRefreshTokenValid } from './tokenUtils' +import { getAccessToken, getRefreshToken, removeTokens, removeAllTokens, isAccessTokenValid, isRefreshTokenValid } from './tokenUtils' // 인증 상태 관리 서비스 export class AuthService { @@ -40,6 +40,11 @@ export class AuthService { const accessToken = getAccessToken() const refreshToken = getRefreshToken() + // 토큰이 없는 경우 + if (!accessToken && !refreshToken) { + return false + } + // 액세스 토큰이 유효한 경우 if (accessToken && isAccessTokenValid()) { return true @@ -50,12 +55,19 @@ export class AuthService { return true } + // 모든 토큰이 만료된 경우 토큰 제거 + if ((accessToken && !isAccessTokenValid()) && (refreshToken && !isRefreshTokenValid())) { + console.log('모든 토큰이 만료되었습니다. 토큰을 제거합니다.') + removeAllTokens() + this.notifyAuthStateChange(false) + } + return false } // 로그아웃 처리 public logout(): void { - removeTokens() + removeAllTokens() this.notifyAuthStateChange(false) // 로그인 페이지로 리다이렉트 diff --git a/src/utils/tokenUtils.ts b/src/utils/tokenUtils.ts index 6ee468e..fa21ed9 100644 --- a/src/utils/tokenUtils.ts +++ b/src/utils/tokenUtils.ts @@ -14,12 +14,24 @@ export const getRefreshToken = (): string | null => { return localStorage.getItem('refreshToken') } -// 토큰 제거 +// 토큰 제거 (로컬 스토리지에서) export const removeTokens = () => { localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') } +// 모든 토큰 제거 (로컬 스토리지 + 쿠키) +export const removeAllTokens = () => { + // 로컬 스토리지에서 토큰 제거 + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + + // 쿠키에서도 토큰 제거 + removeTokensFromCookie() + + console.log('모든 토큰이 제거되었습니다.') +} + // 쿠키에 토큰 저장 (보안 강화 옵션) export const setTokensInCookie = (accessToken: string, refreshToken: string) => { const expires = new Date()