Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -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('')
Expand All @@ -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', {
Expand All @@ -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']
Expand All @@ -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)
}
Expand Down Expand Up @@ -113,7 +110,7 @@ const Login: React.FC = () => {
}`}
onClick={isLoading ? undefined : handleLogin}
>
{isLoading ? '로그인 중...' : '로그인'}
{isLoading ? '접속 중...' : '로그인'}
</div>
</div>
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/pages/admin/SomkatonApplicants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getSomkathonParticipant,
listSomkathonParticipants,
} from './adminService'
import { useAuth } from '../../hooks/useAuth'

const SomkatonApplicants: React.FC = () => {
const [applicants, setApplicants] = useState<SomkathonApplicantListItem[]>([])
Expand All @@ -13,15 +14,22 @@ const SomkatonApplicants: React.FC = () => {
)
const [selectedId, setSelectedId] = useState<number | null>(null)
const [count, setCount] = useState<number>(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)
Expand All @@ -33,7 +41,7 @@ const SomkatonApplicants: React.FC = () => {

useEffect(() => {
getData()
}, [])
}, [isAuthenticated, isLoading])

// 지원자 상세 조회
const toggleDetail = async (id: number) => {
Expand Down
18 changes: 11 additions & 7 deletions src/utils/apiClient.ts
Original file line number Diff line number Diff line change
@@ -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) ||
Expand Down Expand Up @@ -43,7 +43,7 @@ const refreshAccessToken = async (): Promise<string | null> => {
// 리프레시 토큰이 유효하지 않은 경우 (401, 403 등)
if (error.response?.status === 401 || error.response?.status === 403) {
console.log('리프레시 토큰이 유효하지 않습니다. 로그아웃 처리합니다.')
removeTokens()
removeAllTokens()
}

return null
Expand All @@ -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}`
}

Expand Down Expand Up @@ -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'
}
Expand All @@ -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'
}
Expand Down
16 changes: 14 additions & 2 deletions src/utils/authService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAccessToken, getRefreshToken, removeTokens, isAccessTokenValid, isRefreshTokenValid } from './tokenUtils'
import { getAccessToken, getRefreshToken, removeTokens, removeAllTokens, isAccessTokenValid, isRefreshTokenValid } from './tokenUtils'

// 인증 상태 관리 서비스
export class AuthService {
Expand Down Expand Up @@ -40,6 +40,11 @@ export class AuthService {
const accessToken = getAccessToken()
const refreshToken = getRefreshToken()

// 토큰이 없는 경우
if (!accessToken && !refreshToken) {
return false
}

// 액세스 토큰이 유효한 경우
if (accessToken && isAccessTokenValid()) {
return true
Expand All @@ -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)

// 로그인 페이지로 리다이렉트
Expand Down
14 changes: 13 additions & 1 deletion src/utils/tokenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down