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
5,086 changes: 3,724 additions & 1,362 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.0",
"@mui/material": "^7.0.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"@mui/x-date-pickers": "^8.0.0",
"@reduxjs/toolkit": "^2.0.0",
"axios": "^1.7.0",
"aws-amplify": "^6.15.9",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.0.0",
"react-router-dom": "^7.0.0"
"react-router-dom": "^7.12.0"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
Expand Down
220 changes: 143 additions & 77 deletions src/App.jsx

Large diffs are not rendered by default.

47 changes: 37 additions & 10 deletions src/api/axios.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import axios from 'axios'

const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://gc8l9ijhzc.execute-api.ap-northeast-2.amazonaws.com/dev'

const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080',
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
Expand All @@ -10,12 +12,20 @@ const api = axios.create({

// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
async (config) => {
try {
// Cognito 세션에서 토큰 가져오기
const session = await fetchAuthSession()
const token = localStorage.getItem('accessToken')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
} catch (error) {
return config
}
return config
},
(error) => {
return Promise.reject(error)
Expand All @@ -25,10 +35,27 @@ api.interceptors.request.use(
// Response interceptor
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
window.location.href = '/login'

async (error) => {
const originalRequest = error.config

// 401 에러 && 재시도하지 않을 경우
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true

try {
// 토큰 갱신 시도
const session = await fetchAuthSession({ forceRefresh: true })
const newToken = session.tokens?.accessToken?.toString()

if (newToken) {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
return api(originalRequest)
}
} catch (refreshError) {
// 토큰 갱신 실패 시 로그인 페이지로 리다이렉트
window.location.href = '/login'
}
}
return Promise.reject(error)
}
Expand Down
22 changes: 22 additions & 0 deletions src/aws-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const awsConfig = {
Auth: {
Cognito: {
userPoolId: 'ap-northeast-2_ezDwzFCzR',
userPoolClientId: '4ns077jcr1pkue2vvisr6qdpu5',

loginWith: {
email: true,
},

signUpVerificationMethod: 'code',

userAttributes: {
email: {
required: true,
},
},
}
}
};

export default awsConfig;
206 changes: 206 additions & 0 deletions src/contexts/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'
import {
signIn,
signUp,
signOut,
confirmSignUp,
getCurrentUser,
fetchAuthSession,
resendSignUpCode,
} from 'aws-amplify/auth'

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [isAuthenticated, setIsAuthenticated] = useState(false)

useEffect(() => {
checkAuthUser()
}, [])

// 현재 인증된 사용자 확인
const checkAuthUser = async () => {
try {
const currentUser = await getCurrentUser()

setUser({
...currentUser,
email: currentUser.signInDetails?.loginId || currentUser.username,
})
setIsAuthenticated(true)
} catch (error) {
// 로그인되지 않은 상태
setUser(null)
setIsAuthenticated(false)
} finally {
setIsLoading(false)
}
}

// 로그인
const login = useCallback(async (email, password) => {
try {
const result = await signIn({ username: email, password })

if (result.isSignedIn) {
await checkAuthUser()
return { success: true }
}

return {
success: false,
message: '로그인에 실패했습니다.'
}
} catch (error) {
return {
success: false,
message: getAuthErrorMessage(error)
}
}
}, [])

// 회원가입
const register = useCallback(async (email, password) => {
try {
const result = await signUp({
username: email,
password,
options: {
userAttributes: {
email: email,
}
}
})

return {
success: true,
nextStep: result.nextStep,
message: '인증 코드가 이메일로 발송되었습니다.'
}
} catch (error) {
console.error('회원가입 실패:', error)
return {
success: false,
message: getAuthErrorMessage(error)
}
}
}, [])

// 이메일 인증 코드 확인
const confirmEmail = useCallback(async (email, code) => {
try {
const result = await confirmSignUp({
username: email,
confirmationCode: code,
})

return {
success: true,
message: '회원가입이 완료되었습니다! 로그인해주세요.'
}
} catch (error) {
console.error('이메일 인증 코드 확인 실패:', error)
return {
success: false,
message: getAuthErrorMessage(error)
}
}
}, [])

// 인증 코드 재전송
const resendCode = useCallback(async (email) => {
try {
await resendSignUpCode({ username: email })
return {
success: true,
message: '인증 코드가 재전송되었습니다.'
}
} catch (error) {
console.error('인증 코드 재전송 실패:', error)
return {
success: false,
message: getAuthErrorMessage(error)
}
}
}, [])


// 로그아웃
const logout = useCallback(async () => {
try {
await signOut()
setUser(null)
setIsAuthenticated(false)
return { success: true }
} catch (error) {
console.error('Logout error:', error)
return {
success: false,
message: '로그아웃 실패'
}
}
}, [])

const value = useMemo(() => ({
user,
isLoading,
isAuthenticated,
login,
register,
confirmEmail,
resendCode,
logout,
checkAuthUser,
}), [
user,
isLoading,
isAuthenticated,
login,
register,
confirmEmail,
resendCode,
logout,
checkAuthUser,
])

return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth는 AuthProvider 안에서 사용해야 합니다')
}
return context
}

// Cognito 에러 메시지 변환 헬퍼
function getAuthErrorMessage(error) {
const errorMessages = {
'UserNotFoundException': '등록되지 않은 이메일입니다.',
'NotAuthorizedException': '이메일 또는 비밀번호가 올바르지 않습니다.',
'UserNotConfirmedException': '이메일 인증이 완료되지 않았습니다.',
'UsernameExistsException': '이미 사용 중인 이메일입니다.',
'InvalidPasswordException': '비밀번호 형식이 올바르지 않습니다. (8자 이상, 대소문자, 숫자, 특수문자 포함)',
'CodeMismatchException': '인증 코드가 올바르지 않습니다.',
'ExpiredCodeException': '인증 코드가 만료되었습니다. 다시 요청해주세요.',
'LimitExceededException': '요청 횟수를 초과했습니다. 잠시 후 다시 시도해주세요.',
'TooManyRequestsException': '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
'InvalidParameterException': '입력 정보가 올바르지 않습니다.',
}

// Cognito 에러 코드로 메시지 찾기
const errorName = error.name || error.code
if (errorMessages[errorName]) {
return errorMessages[errorName]
}

// 기본 메시지
return error.message || '오류가 발생했습니다. 다시 시도해주세요.'
}
Loading