Skip to content

Commit 4f3080e

Browse files
authored
Merge pull request #112 from Ureca-Mini-Project-Team4/feature/api-user
FEAT: 인증 상태 로직 관리 수정 및 User API 모듈화
2 parents 6a1d7a0 + d9553e7 commit 4f3080e

File tree

11 files changed

+197
-121
lines changed

11 files changed

+197
-121
lines changed

react-app/src/apis/axiosInstance.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import axios from 'axios';
2+
import { clearAuth } from '@/store/slices/authSlice';
23
import { store } from '@/store';
3-
import { setAuth, clearAuth } from '@/store/slices/authSlice';
44

5-
// axios 인스턴스 생성 (baseURL, 헤더 등)
5+
// 인스턴스 생성
66
const axiosInstance = axios.create({
77
baseURL: import.meta.env.VITE_API_BASE_URL,
88
withCredentials: true,
9-
headers: { 'Content-Type': 'application/json' },
9+
headers: {
10+
'Content-Type': 'application/json',
11+
},
1012
});
1113

1214
// 요청 시 accessToken이 있으면 Authorization 헤더에 자동 추가
1315
axiosInstance.interceptors.request.use((config) => {
14-
const token = store.getState().auth.accessToken;
15-
if (token) config.headers['Authorization'] = `Bearer ${token}`;
16+
const accessToken = localStorage.getItem('accessToken');
17+
if (accessToken) {
18+
config.headers['Authorization'] = `Bearer ${accessToken}`;
19+
}
1620
return config;
1721
});
1822

@@ -21,27 +25,40 @@ axiosInstance.interceptors.response.use(
2125
(response) => response,
2226
async (error) => {
2327
const originalRequest = error.config;
28+
2429
if (error.response?.status === 401 && !originalRequest._retry) {
2530
originalRequest._retry = true;
2631
try {
27-
const refreshToken = store.getState().auth.refreshToken;
32+
const refreshToken = localStorage.getItem('refreshToken');
33+
34+
if (!refreshToken) throw new Error('No refresh token');
35+
2836
const res = await axios.post(
29-
'/api/token/refresh',
37+
`${import.meta.env.VITE_API_BASE_URL}/api/token/refresh`,
3038
{},
3139
{
32-
headers: { 'Refresh-Token': refreshToken },
40+
headers: {
41+
'Refresh-Token': refreshToken,
42+
'Content-Type': 'application/json',
43+
},
3344
},
3445
);
35-
const { accessToken } = res.data;
36-
store.dispatch(setAuth({ ...store.getState().auth, accessToken }));
37-
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
46+
47+
const newAccessToken = res.data.accessToken;
48+
localStorage.setItem('accessToken', newAccessToken);
49+
50+
// 새 토큰으로 재시도
51+
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
3852
return axiosInstance(originalRequest);
3953
} catch (refreshError) {
54+
// 리프레시 실패 → 로그아웃 처리
4055
store.dispatch(clearAuth());
56+
localStorage.clear();
4157
window.location.href = '/login';
4258
return Promise.reject(refreshError);
4359
}
4460
}
61+
4562
return Promise.reject(error);
4663
},
4764
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import axiosInstance from '../axiosInstance';
2+
3+
export interface ChangePasswordRequest {
4+
old_password: string;
5+
new_password: string;
6+
}
7+
8+
export async function changePassword(userId: string, body: ChangePasswordRequest): Promise<void> {
9+
const response = await axiosInstance.patch(`/user/${userId}`, body);
10+
return response.data;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import axiosInstance from '../axiosInstance';
2+
3+
export interface UserInfoRequest {
4+
userId: number;
5+
}
6+
7+
export interface UserInfoResponse {
8+
userId: number;
9+
username: string;
10+
randomNickname: string;
11+
selected: boolean;
12+
voted: boolean;
13+
token?: string | null;
14+
}
15+
16+
export async function getUserInfo(userId: string): Promise<UserInfoResponse> {
17+
const response = await axiosInstance.post(`/user/${userId}`);
18+
return response.data;
19+
}

react-app/src/apis/user/login.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
import axiosInstance from '../axiosInstance';
2+
import { UserInfoResponse } from './getUserInfo';
23

34
export interface LoginRequest {
45
username: string;
56
password: string;
67
}
78

89
export interface LoginResponse {
9-
user: {
10-
userId: number;
11-
voted: boolean;
12-
};
10+
user: UserInfoResponse | null;
1311
accessToken: string | null;
1412
refreshToken: string | null;
1513
}
1614

1715
export async function login(params: LoginRequest): Promise<LoginResponse> {
1816
const response = await axiosInstance.post('/user/login', params);
19-
const accessToken = response.headers['authorization']?.replace('Bearer ', '');
20-
const refreshToken = response.headers['refresh-token'];
17+
const accessToken = response.headers['authorization']?.replace('Bearer ', '') ?? null;
18+
const refreshToken = response.headers['refresh-token'] ?? null;
2119

2220
return {
23-
user: response.data.user,
21+
user: response.data.user as UserInfoResponse,
2422
accessToken,
2523
refreshToken,
2624
};

react-app/src/hook/useAuth.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
1-
import { useSelector, useDispatch } from 'react-redux';
1+
import { login } from '@/apis/user/login';
2+
import { useDispatch, useSelector } from 'react-redux';
23
import { RootState } from '@/store';
34
import { setAuth, clearAuth } from '@/store/slices/authSlice';
5+
import { useNavigate } from 'react-router-dom';
6+
import { UserInfoResponse } from '@/apis/user/getUserInfo';
47

5-
// 현재 인증 상태를 반환하는 커스텀 훅
6-
export const useAuth = () => {
7-
const auth = useSelector((state: RootState) => state.auth);
8+
export function useAuth() {
89
const dispatch = useDispatch();
10+
const navigate = useNavigate();
11+
const user = useSelector((state: RootState) => state.auth.user);
912

10-
// login → setAuth 디스패치, logout → clearAuth 디스패치
11-
return {
12-
auth,
13-
login: (data: any) => dispatch(setAuth(data)),
14-
logout: () => dispatch(clearAuth()),
13+
const handleLogin = async (username: string, password: string) => {
14+
try {
15+
const { user, accessToken, refreshToken } = await login({ username, password });
16+
17+
if (user && accessToken && refreshToken) {
18+
dispatch(setAuth({ user: user as UserInfoResponse }));
19+
localStorage.setItem('accessToken', accessToken);
20+
localStorage.setItem('refreshToken', refreshToken);
21+
localStorage.setItem('userId', String(user.userId));
22+
navigate('/main');
23+
} else {
24+
throw new Error('로그인 정보가 올바르지 않습니다.');
25+
}
26+
} catch (error) {
27+
console.error('로그인 실패:', error);
28+
throw error;
29+
}
30+
};
31+
32+
const logout = () => {
33+
dispatch(clearAuth());
34+
localStorage.clear();
1535
};
16-
};
36+
37+
const isLoggedIn = Boolean(user);
38+
39+
return { user, isLoggedIn, login: handleLogin, logout };
40+
}

react-app/src/pages/ChangePW.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,55 @@
1+
import { useState } from 'react';
2+
import { changePassword } from '@/apis/user/changePassword';
3+
14
const ChangePW = () => {
2-
return (
3-
<div>
4-
5-
</div>
6-
);
5+
const [oldPassword, setOldPassword] = useState('');
6+
const [newPassword, setNewPassword] = useState('');
7+
8+
const handleChangePassword = async () => {
9+
try {
10+
const userId = localStorage.getItem('userId');
11+
if (!userId) throw new Error('로그인 정보 없음');
12+
13+
await changePassword(userId, {
14+
old_password: oldPassword,
15+
new_password: newPassword,
16+
});
17+
18+
alert('비밀번호가 변경되었습니다.');
19+
setOldPassword('');
20+
setNewPassword('');
21+
} catch (err) {
22+
alert('비밀번호 변경 실패');
23+
console.error(err);
24+
}
25+
};
26+
27+
return (
28+
<div className="max-w-md mx-auto ">
29+
<h2 className="text-xl font-bold mb-6 text-center">비밀번호 변경 테스트</h2>
30+
<div className="mb-4">
31+
<label className="font-pm block mb-1">현재 비밀번호</label>
32+
<input
33+
type="password"
34+
value={oldPassword}
35+
onChange={(e) => setOldPassword(e.target.value)}
36+
className="w-full px-3 py-2 border rounded focus:ring focus:outline-none"
37+
/>
38+
</div>
39+
<div className="mb-6">
40+
<label className="font-pm block mb-1">새 비밀번호</label>
41+
<input
42+
type="password"
43+
value={newPassword}
44+
onChange={(e) => setNewPassword(e.target.value)}
45+
className="w-full px-3 py-2 border rounded focus:ring focus:outline-none"
46+
/>
47+
</div>
48+
<button onClick={handleChangePassword} className="w-full py-2 text-white rounded ">
49+
변경하기
50+
</button>
51+
</div>
52+
);
753
};
854

9-
export default ChangePW;
55+
export default ChangePW;

react-app/src/pages/Home.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
import { useToast } from '@/hook/useToast';
2-
31
export default function Home() {
4-
const { showToast } = useToast();
5-
6-
const handleButtonClick = () => {
7-
showToast('버튼이 클릭되었습니다', 'success');
8-
};
9-
10-
return (
11-
<div>
12-
<h1>Hello</h1>
13-
<button onClick={handleButtonClick}>토스트 표시</button>
14-
</div>
15-
);
2+
return <div></div>;
163
}

react-app/src/pages/Login.tsx

Lines changed: 38 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,61 @@
11
import { useState } from 'react';
2-
import { useDispatch } from 'react-redux';
3-
import { clearAuth, setAuth } from '@/store/slices/authSlice';
4-
import { login } from '@/apis/user/login';
5-
import { useSelector } from 'react-redux';
6-
import { RootState } from '@/store';
2+
import { useAuth } from '@/hook/useAuth';
73

84
const Login = () => {
9-
const accessToken = useSelector((state: RootState) => state.auth.accessToken);
10-
11-
const dispatch = useDispatch();
5+
const { login, logout } = useAuth();
126
const [username, setUsername] = useState('');
137
const [password, setPassword] = useState('');
148

159
const handleLogin = async () => {
1610
try {
17-
const response = await login({ username, password });
18-
const { user, accessToken, refreshToken } = response;
19-
20-
dispatch(
21-
setAuth({
22-
user,
23-
accessToken,
24-
refreshToken,
25-
}),
26-
);
27-
28-
setUsername('');
29-
setPassword('');
30-
31-
alert('로그인 성공!');
32-
} catch (error) {
11+
await login(username, password);
12+
} catch (e) {
3313
alert('로그인 실패');
34-
console.error(error);
14+
console.error(e);
3515
}
3616
};
3717

38-
const handleLogout = () => {
39-
dispatch(clearAuth());
40-
localStorage.removeItem('accessToken');
41-
};
42-
43-
if (!accessToken) {
44-
return (
45-
<div className="flex flex-col items-center justify-center h-screen gap-4">
46-
<h2 className="text-2xl font-bold">로그인 테스트 페이지</h2>
47-
<input
48-
type="text"
49-
placeholder="아이디"
50-
value={username}
51-
onChange={(e) => setUsername(e.target.value)}
52-
className="p-2 border rounded"
53-
/>
54-
<input
55-
type="password"
56-
placeholder="비밀번호"
57-
value={password}
58-
onChange={(e) => setPassword(e.target.value)}
59-
className="p-2 border rounded"
60-
/>
18+
return (
19+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
20+
<div className="bg-white shadow-xl rounded-2xl p-8 w-full max-w-sm">
21+
<h2 className="font-gumi text-2xl font-bold text-center mb-6">
22+
<span className="text-(--color-primary-base)"></span>로 정했다!
23+
</h2>
24+
<div className="mb-4">
25+
<label className="font-pm block text-gray-700 mb-2">아이디</label>
26+
<input
27+
type="text"
28+
value={username}
29+
onChange={(e) => setUsername(e.target.value)}
30+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-(--color-primary-base)/70"
31+
placeholder="아이디를 입력하세요"
32+
/>
33+
</div>
34+
<div className="mb-6">
35+
<label className="font-pm block text-gray-700 mb-2">비밀번호</label>
36+
<input
37+
type="password"
38+
value={password}
39+
onChange={(e) => setPassword(e.target.value)}
40+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-(--color-primary-base)/70"
41+
placeholder="비밀번호를 입력하세요"
42+
/>
43+
</div>
6144
<button
6245
onClick={handleLogin}
63-
className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600"
46+
className="w-full py-2 bg-(--color-primary-base) text-white font-semibold rounded-lg hover:bg-(--color-primary-hover) transition duration-200"
6447
>
6548
로그인
6649
</button>
67-
</div>
68-
);
69-
} else {
70-
return (
71-
<div>
72-
<div>로그인 중 입니다.</div>
73-
<button className="bg-amber-300" onClick={handleLogout}>
50+
<button
51+
onClick={logout}
52+
className="mt-4 w-full py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition duration-200"
53+
>
7454
로그아웃
7555
</button>
7656
</div>
77-
);
78-
}
57+
</div>
58+
);
7959
};
8060

8161
export default Login;

0 commit comments

Comments
 (0)