diff --git a/src/apis/auth.api.ts b/src/apis/auth.api.ts index 8c22891..6cabed1 100644 --- a/src/apis/auth.api.ts +++ b/src/apis/auth.api.ts @@ -1,70 +1,22 @@ -import httpClient from './http.api'; // Axios 인스턴스 가져오기 -import { JoinProps } from '@/pages/JoinPage'; import { LoginProps } from '@/pages/LoginPage'; +import { JoinProps } from '@/pages/JoinPage'; +import httpClient from './http.api'; -// 회원가입 요청 -export const join = async (userData: JoinProps) => { - try { - const { data, status } = await httpClient.post('/api/users/join', userData); - - if (status === 200) { - console.log('회원가입 성공:', data); - return data; - } else if (status === 400 && data.message.includes('입력해주세요')) { - console.error('필수 입력값 없음:', data.message); - throw new Error(data.message); - } else if (status === 400 && data.message.includes('중복된')) { - console.error('중복 오류:', data.message); - throw new Error(data.message); - } else { - throw new Error('알 수 없는 회원가입 에러가 발생했습니다.'); - } - } catch (error) { - console.error('회원가입 요청 중 에러 발생:', error); - throw new Error('회원가입 요청에 실패했습니다. 관리자에게 문의하세요.'); - } +export const join = async (data: JoinProps) => { + const response = await httpClient.post('/api/users/join', data); + return response.data; }; -interface LoginResponse { +export interface LoginResponse { message: any; success: boolean; token?: string; } -// 로그인 요청 -export const login = async (data: LoginProps): Promise => { - try { - const { data: responseData, status } = await httpClient.post( - '/api/users/login', - data - ); - - if (status === 200 && responseData.success) { - console.log('로그인 성공:', responseData); - return responseData; - } else if ( - status === 400 && - responseData.message.includes('입력해주세요') - ) { - console.error('필수 입력값 없음:', responseData.message); - throw new Error(responseData.message); - } else if ( - status === 404 && - responseData.message.includes('등록되지 않은 이메일') - ) { - console.error('등록되지 않은 이메일:', responseData.message); - throw new Error(responseData.message); - } else if ( - status === 401 && - responseData.message.includes('비밀번호가 틀렸습니다') - ) { - console.error('비밀번호 오류:', responseData.message); - throw new Error(responseData.message); - } else { - throw new Error('알 수 없는 로그인 에러가 발생했습니다.'); - } - } catch (error) { - console.error('로그인 요청 중 에러 발생:', error); - throw new Error('로그인 요청에 실패했습니다. 관리자에게 문의하세요.'); - } +export const login = async (data: LoginProps) => { + const response = await httpClient.post( + '/api/users/login', + data + ); + return response.data; }; diff --git a/src/apis/http.api.ts b/src/apis/http.api.ts index 67909a3..56772c4 100644 --- a/src/apis/http.api.ts +++ b/src/apis/http.api.ts @@ -1,5 +1,5 @@ -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -import { getToken, removeToken } from '@/store/slices/authSlice'; // 토큰 유틸리티 함수 import +import axios, { AxiosInstance, AxiosRequestConfig, AxiosHeaders } from 'axios'; +import { getToken, removeToken } from '@/utils/token'; // 토큰 유틸리티 함수 import import { logout } from '@/store/slices/authSlice'; import { store } from '@/store/store'; @@ -64,42 +64,5 @@ export const createClient = (config?: AxiosRequestConfig): AxiosInstance => { // 기본 Axios 인스턴스 생성 export const httpClient = createClient(); -// API 요청 함수 -export const authApi = { - join: async (data: { email: string; password: string; nickname: string }) => { - try { - const response = await httpClient.post('/api/signup', data); - return response.data; - } catch (error) { - console.error('Signup API Error:', error); - throw error; - } - }, - login: async (data: { email: string; password: string }) => { - try { - const response = await httpClient.post('/api/login', data); - const { token } = response.data; - - // 토큰 저장 - localStorage.setItem('accessToken', token); // 필요 시 유틸리티 함수 사용 가능 - return response.data; - } catch (error) { - console.error('Login API Error:', error); - throw error; - } - }, - logout: async () => { - try { - await httpClient.post('/api/logout'); - removeToken(); // 토큰 삭제 - store.dispatch(logout()); // Redux 상태 초기화 - window.location.href = '/login'; // 로그인 페이지로 리다이렉트 - } catch (error) { - console.error('Logout API Error:', error); - throw error; - } - }, -}; - // 기본 Axios 인스턴스 내보내기 export default httpClient; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 2575c59..89bd08d 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -3,8 +3,9 @@ import { useNavigate } from 'react-router'; import qublogo from '@/assets/qublogo.svg'; import Input from '@/components/atoms/Input'; import { useForm } from 'react-hook-form'; -import { login } from '@/apis/auth.api'; -import { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; // Redux 추가 +import { AppDispatch, RootState } from '@/store/store'; // Redux 타입 가져오기 +import { loginThunk } from '@/store/slices/authSlice'; // Thunk 가져오기 export interface LoginProps { email: string; @@ -13,28 +14,33 @@ export interface LoginProps { function LoginPage() { const navigate = useNavigate(); + const dispatch = useDispatch(); + + const { loading, error, isLoggedIn } = useSelector( + (state: RootState) => state.auth + ); + const { register, handleSubmit, formState: { errors }, } = useForm(); - const [isLoading, setIsLoading] = useState(false); // 로딩 상태 추가 - const onSubmit = async (data: LoginProps) => { - setIsLoading(true); - try { - await login(data); + const result = await dispatch(loginThunk(data)); + + if (loginThunk.fulfilled.match(result)) { alert('로그인에 성공했습니다!'); navigate('/'); // 로그인 성공 시 메인 페이지로 이동 - } catch (error: any) { - console.error('로그인 실패:', error); - alert(error.message || '로그인에 실패했습니다. 다시 시도해주세요.'); - } finally { - setIsLoading(false); + } else { + alert(result.payload || '로그인에 실패했습니다. 다시 시도해주세요.'); } }; + // 로그인 상태 확인 후 리다이렉트 처리 + if (isLoggedIn) { + navigate('/'); + } return ( @@ -61,8 +67,10 @@ function LoginPage() { {errors.password && {errors.password.message}} - - {isLoading ? '로그인 중...' : '로그인'} + {error && {error}} + + + {loading ? '로그인 중...' : '로그인'} diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts index cad3317..4535169 100644 --- a/src/store/slices/authSlice.ts +++ b/src/store/slices/authSlice.ts @@ -1,208 +1,78 @@ -// src/store/slices/authSlice.ts - -import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; -import axios from 'axios'; +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { login } from '@/apis/auth.api'; +import { LoginRequest, LoginResponse } from '@/types/auth'; +import { DecodedToken, LoginThunkResponse } from '@/types/auth'; // 디코딩된 토큰 타입 가져오기 import { jwtDecode } from 'jwt-decode'; -// 사용자 정보 인터페이스 -interface User { - email: string; - nickname: string; - role: 'user' | 'admin'; -} - -// 토큰 인터페이스 -interface Tokens { - accessToken: string; -} - -// 인증 상태 인터페이스 -export interface AuthState { - isLoggedIn: boolean; - user: User | null; - tokens: Tokens | null; - error: string | null; -} - -// 로그인 요청 페이로드 인터페이스 -interface LoginPayload { - email: string; - password: string; -} - -// 회원가입 요청 페이로드 인터페이스 -interface SignupPayload { - email: string; - password: string; - nickname: string; -} - -// JWT 디코딩 인터페이스 -interface DecodedToken { - email: string; - nickname: string; - roles: string[]; - exp: number; -} - -// LocalStorage 관련 유틸리티 함수 -export const getToken = (): string | null => - localStorage.getItem('accessToken'); -export const setToken = (token: string): void => - localStorage.setItem('accessToken', token); -export const removeToken = (): void => localStorage.removeItem('accessToken'); - -export const getNickname = (): string | null => - localStorage.getItem('nickname'); -export const setNickname = (nickname: string): void => - localStorage.setItem('nickname', nickname); -export const removeNickname = (): void => localStorage.removeItem('nickname'); - -// 비동기 Thunk 액션 생성 - -// 회원가입 Thunk -export const signup = createAsyncThunk< - { success: boolean }, - SignupPayload, - { rejectValue: string } ->('auth/signup', async (data, thunkAPI) => { - try { - console.log('회원가입 요청 데이터:', data); - const response = await axios.post('/api/signup', data); - console.log('회원가입 성공 응답:', response.data); - return response.data; - } catch (error: any) { - console.error( - '회원가입 실패:', - error.response?.data?.message || error.message - ); - return thunkAPI.rejectWithValue( - error.response?.data?.message || '회원가입 실패' - ); - } -}); - -// 로그인 Thunk -export const login = createAsyncThunk< - { user: User; tokens: Tokens }, - LoginPayload, - { rejectValue: string } ->('auth/login', async (credentials, thunkAPI) => { - try { - console.log('로그인 요청 데이터:', credentials); - const response = await axios.post('/api/login', credentials); - const { token } = response.data; - - // JWT 디코딩하여 사용자 정보 추출 - const decoded: DecodedToken = jwtDecode(token); - const role = decoded.roles.includes('admin') ? 'admin' : 'user'; - - const userData: User = { - email: decoded.email, - nickname: decoded.nickname, - role, - }; - - const tokensData: Tokens = { - accessToken: token, - }; +// 초기 상태 +const initialState = { + isLoggedIn: false, + token: null as string | null, + decodedToken: null as DecodedToken | null, // 디코딩된 토큰 정보 + loading: false, + error: null as string | null, +}; - // 로컬스토리지에 토큰 저장 - setToken(tokensData.accessToken); - setNickname(userData.nickname); +export const loginThunk = createAsyncThunk( + 'auth/login', + async (credentials, thunkAPI) => { + try { + const response: LoginResponse = await login(credentials); - console.log('로그인 성공:', { user: userData, tokens: tokensData }); + // 토큰이 없는 경우 처리 + if (!response.token) { + throw new Error('Token is missing from the server response.'); + } - return { user: userData, tokens: tokensData }; - } catch (error: any) { - console.error( - '로그인 실패:', - error.response?.data?.message || error.message - ); - return thunkAPI.rejectWithValue( - error.response?.data?.message || '로그인 실패' - ); + // JWT 디코딩 + const decodedToken = jwtDecode(response.token); + + return { + token: response.token, + decodedToken, + }; + } catch (error: any) { + return thunkAPI.rejectWithValue( + error.response?.data?.message || '로그인 요청 실패' + ); + } } -}); - -// 초기 상태 설정 -const initialState: AuthState = { - isLoggedIn: !!getToken(), - user: getNickname() - ? { - email: '', - nickname: getNickname() || '', - role: 'user', - } - : null, - tokens: getToken() - ? { - accessToken: getToken()!, - } - : null, - error: null, -}; +); -// 슬라이스 생성 +// Slice 생성 const authSlice = createSlice({ name: 'auth', initialState, reducers: { - // 로그아웃 액션 - logout: (state) => { - console.log('로그아웃 처리'); + logout(state) { state.isLoggedIn = false; - state.user = null; - state.tokens = null; - state.error = null; - removeToken(); - removeNickname(); - }, - // 에러 설정 액션 - setError: (state, action: PayloadAction) => { - state.error = action.payload; - }, - // 에러 클리어 액션 - clearError: (state) => { + state.token = null; state.error = null; + + localStorage.removeItem('token'); }, }, extraReducers: (builder) => { - // 회원가입 - builder.addCase(signup.pending, (state) => { - console.log('회원가입 진행 중...'); - state.error = null; - }); - builder.addCase(signup.fulfilled, (state) => { - console.log('회원가입 성공'); - state.error = null; - }); - builder.addCase(signup.rejected, (state, action) => { - console.log('회원가입 실패:', action.payload || '알 수 없는 오류'); - state.error = action.payload || '회원가입 실패'; - }); - - // 로그인 - builder.addCase(login.pending, (state) => { - console.log('로그인 진행 중...'); - state.error = null; - }); - builder.addCase( - login.fulfilled, - (state, action: PayloadAction<{ user: User; tokens: Tokens }>) => { - console.log('로그인 성공'); + builder + .addCase(loginThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(loginThunk.fulfilled, (state, action) => { + state.loading = false; state.isLoggedIn = true; - state.user = action.payload.user; - state.tokens = action.payload.tokens; - } - ); - builder.addCase(login.rejected, (state, action) => { - console.log('로그인 실패:', action.payload || '알 수 없는 오류'); - state.error = action.payload || '로그인 실패'; - }); + state.token = action.payload.token; + state.decodedToken = action.payload.decodedToken; // 디코딩된 정보 저장 + + // 로컬스토리지에 토큰 저장 + localStorage.setItem('token', action.payload.token); + }) + .addCase(loginThunk.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; // 에러 메시지 저장 + }); }, }); -// 액션 및 리듀서 내보내기 -export const { logout, setError, clearError } = authSlice.actions; +export const { logout } = authSlice.actions; export default authSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts index 5db4517..b1fe3eb 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -9,3 +9,5 @@ export const store = configureStore({ export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; + +export default store; diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..8f76bf5 --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,53 @@ +export interface User { + email: string; + nickname: string; + role: 'user' | 'admin'; +} + +// 토큰 인터페이스 +interface Tokens { + accessToken: string; +} + +export interface AuthState { + isLoggedIn: boolean; + user: User | null; + tokens: Tokens | null; + error: string | null; +} + +// 로그인 요청 데이터 타입 +export interface LoginRequest { + email: string; + password: string; +} + +export interface AuthResponse { + success: boolean; + token?: string; + user?: User; +} + +export interface APIError { + message: string; +} + +export interface DecodedToken { + email: string; + nickname: string; + role: string | string[]; // 단일 문자열 또는 배열 + iat: number; + exp: number; +} + +// Thunk 반환 타입 +export interface LoginThunkResponse { + token: string; // JWT 토큰 + decodedToken: DecodedToken; // 디코딩된 데이터 +} + +export interface LoginResponse { + message: any; + success: boolean; + token?: string; +} diff --git a/src/utils/token.ts b/src/utils/token.ts new file mode 100644 index 0000000..025f9ed --- /dev/null +++ b/src/utils/token.ts @@ -0,0 +1,12 @@ +// 로컬스토리지에서 토큰 가져오기 +export const getToken = (): string | null => { + return localStorage.getItem('token'); +}; + +export const setToken = (token: string) => { + localStorage.setItem('token', token); +}; + +export const removeToken = () => { + localStorage.removeItem('token'); +};