Skip to content

Commit cae3dca

Browse files
authored
Merge pull request #165 from CampingOn/style/#160-user
Style: #160 프론트엔드 오피스아워 피드백 반영
2 parents 76029f2 + 7c78b68 commit cae3dca

13 files changed

+458
-314
lines changed

src/api/axiosConfig.js

+40-52
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,14 @@ const excludedUrls = [
1818
"/api/mongo/camps/search",
1919
"/api/camps/*/available",
2020
"/api/camps/*",
21-
"/api/keywords"];
21+
"/api/keywords"
22+
];
2223

23-
let isRefreshing = false; // Refresh Token 갱신 중 여부
24-
let refreshSubscribers = []; // 갱신 후 재요청 대기열
25-
26-
// Refresh Token 갱신 시 대기 중인 요청 처리
27-
function onTokenRefreshed(newAccessToken) {
28-
refreshSubscribers.forEach((callback) => callback(newAccessToken));
29-
refreshSubscribers = [];
30-
}
31-
32-
// 갱신 대기열에 요청 추가
33-
function addRefreshSubscriber(callback) {
34-
refreshSubscribers.push(callback);
35-
}
24+
let refreshTokenPromise = null; // Refresh Token 갱신 중일 때 참조할 Promise
3625

3726
// 요청 인터셉터
3827
apiClient.interceptors.request.use(
39-
async (config) => {
28+
(config) => {
4029
const accessToken = localStorage.getItem("accessToken");
4130

4231
if (accessToken) {
@@ -59,7 +48,7 @@ apiClient.interceptors.response.use(
5948
console.log(`✅ 응답: ${response.config.url}`);
6049
return response;
6150
},
62-
async (error) => {
51+
(error) => {
6352
console.error("🚫️ 응답 에러:", error.message);
6453
const originalRequest = error.config;
6554

@@ -72,44 +61,43 @@ apiClient.interceptors.response.use(
7261
if (error.response && error.response.status === 401 && !originalRequest._retry) {
7362
originalRequest._retry = true;
7463

75-
// Refresh 토큰 갱신중인 경우 대기
76-
if (!isRefreshing) {
77-
isRefreshing = true;
78-
79-
try {
80-
// Refresh Token으로 Access Token 재발급
81-
const response = await axios.get(`${baseUrl}api/token/refresh`, {
82-
withCredentials: true, // 쿠키를 통한 인증
83-
});
84-
85-
const newAccessToken = response.data.accessToken;
86-
87-
// 로컬 스토리지에 저장
88-
localStorage.setItem("accessToken", newAccessToken);
89-
90-
// 갱신 대기 중인 요청 처리
91-
onTokenRefreshed(newAccessToken);
92-
93-
isRefreshing = false;
94-
// Authorization Header 업데이트
95-
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
96-
return apiClient(originalRequest);
97-
98-
} catch (refreshError) {
99-
// 재발급 실패 시 로그아웃 처리
100-
localStorage.removeItem("accessToken");
101-
window.location.href = "/login";
102-
isRefreshing = false;
103-
return Promise.reject(refreshError);
104-
}
64+
if (!refreshTokenPromise) {
65+
// **Refresh Token 갱신을 위한 Promise 생성**
66+
refreshTokenPromise = new Promise((resolve, reject) => {
67+
console.log("🔄 Refresh Token으로 새로운 Access Token 발급 요청 중...");
68+
69+
axios.get(`${baseUrl}api/token/refresh`, { withCredentials: true })
70+
.then((response) => {
71+
const newAccessToken = response.data.accessToken;
72+
73+
// 새 Access Token을 로컬 스토리지에 저장
74+
localStorage.setItem("accessToken", newAccessToken);
75+
console.log("🔄 새 Access Token 발급 완료!");
76+
77+
// **모든 대기 요청에 대해 resolve() 호출**
78+
resolve(newAccessToken);
79+
})
80+
.catch((refreshError) => {
81+
console.error("❌ Refresh Token 갱신 실패", refreshError);
82+
localStorage.removeItem("accessToken");
83+
window.location.href = "/login"; // 로그인 페이지로 이동
84+
reject(refreshError);
85+
})
86+
.finally(() => {
87+
// Promise 해제
88+
refreshTokenPromise = null;
89+
});
90+
});
10591
}
10692

107-
// 토큰 갱신 대기
108-
return new Promise((resolve) => {
109-
addRefreshSubscriber((newAccessToken) => {
110-
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
111-
resolve(apiClient(originalRequest));
112-
});
93+
// 모든 대기 중인 요청들이 Promise에 연결되어, 새 토큰을 기다림
94+
return refreshTokenPromise.then((newAccessToken) => {
95+
console.log("🔓 대기 중이던 요청에 새 Access Token 할당");
96+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
97+
return apiClient(originalRequest); // 대기 중이던 요청 재시도
98+
}).catch((refreshError) => {
99+
console.error("❌ 대기 중인 요청도 실패함", refreshError);
100+
return Promise.reject(refreshError);
113101
});
114102
}
115103

src/components/camp/CampBookmarkedCard.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ const CampBookmarkedCard = ({ data, onBookmarkChange }) => {
2222
setImageUrl(randomImage);
2323
}, [thumbImage]);
2424

25-
const handleNameClick = () => {
26-
navigate(`/camps/${campId}`);
27-
};
28-
2925
const toggleLike = async (event) => {
3026
event.stopPropagation(); // 부모의 onClick 이벤트가 실행되지 않도록 중단
3127

@@ -69,9 +65,10 @@ const CampBookmarkedCard = ({ data, onBookmarkChange }) => {
6965
<Typography
7066
variant="h5"
7167
sx={{ marginBottom: 2, fontWeight: 'bold' ,cursor: 'pointer'}}
72-
onClick={handleNameClick}
7368
>
74-
{name}
69+
<a href={`/camps/${campId}`} style={{textDecoration: 'none', color: 'inherit'}}>
70+
{name}
71+
</a>
7572
</Typography>
7673
<Box sx={{ display: "flex", alignItems: "center", marginBottom: 1 }}>
7774
<LocationOnOutlinedIcon sx={{ fontSize: 20, marginRight: 1, color: "green" }} />
+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React, { useState } from 'react';
2+
import { TextField, Button, IconButton, InputAdornment } from '@mui/material';
3+
import { Visibility, VisibilityOff } from '@mui/icons-material';
4+
5+
function CustomInput({
6+
id,
7+
label,
8+
type = 'text',
9+
value,
10+
onChange,
11+
onBlur,
12+
error,
13+
placeholder = '',
14+
buttonText,
15+
onButtonClick,
16+
buttonVisible = false,
17+
successMessage,
18+
disabled,
19+
}) {
20+
const [showPassword, setShowPassword] = useState(false);
21+
22+
const handleTogglePasswordVisibility = () => {
23+
setShowPassword((prev) => !prev);
24+
};
25+
26+
// 테두리 색상 결정 로직
27+
const getBorderColor = () => {
28+
if (error) return '#f6685e';
29+
if (successMessage) return '#16A34A';
30+
return '#e0e0e0';
31+
};
32+
33+
return (
34+
<div>
35+
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
36+
<TextField
37+
id={id}
38+
type={type === 'password' && showPassword ? 'text' : type}
39+
value={value}
40+
label={label}
41+
onChange={onChange}
42+
onBlur={onBlur}
43+
placeholder={placeholder}
44+
disabled={disabled}
45+
variant="outlined"
46+
fullWidth
47+
size="small"
48+
error={Boolean(error)}
49+
helperText={error || successMessage}
50+
InputProps={{
51+
endAdornment: type === 'password' && (
52+
<InputAdornment position="end">
53+
<IconButton
54+
onClick={handleTogglePasswordVisibility}
55+
edge="end"
56+
aria-label="toggle password visibility"
57+
sx={{ fontSize: '1rem', padding: '8px'}}
58+
>
59+
{showPassword ?
60+
<VisibilityOff sx={{ fontSize: '1rem' }} />
61+
: <Visibility sx={{ fontSize: '1rem' }} />}
62+
</IconButton>
63+
</InputAdornment>
64+
),
65+
}}
66+
InputLabelProps={{
67+
sx: {
68+
fontSize: '0.9rem',
69+
'&.Mui-focused': {
70+
fontSize: '1rem',
71+
},
72+
},
73+
}}
74+
FormHelperTextProps={{
75+
sx: {
76+
color: error ? 'red' : successMessage ? 'green' : 'inherit', // 에러는 red, 성공은 green으로 유지
77+
marginTop: '8px',
78+
},
79+
}}
80+
sx={{
81+
'& .MuiOutlinedInput-root': {
82+
borderRadius: '0.375rem',
83+
'& fieldset': {
84+
borderColor: getBorderColor(),
85+
},
86+
'&:hover fieldset': {
87+
borderColor: getBorderColor(),
88+
},
89+
'&.Mui-focused fieldset': {
90+
borderColor: '#ffc400', // 포커스 상태 테두리 색상 고정
91+
},
92+
'& input:focus': {
93+
outline: 'none !important',
94+
boxShadow: 'none !important',
95+
},
96+
},
97+
'& label.Mui-focused': {
98+
color: '#ffc400', // 포커스된 라벨 색상 고정
99+
},
100+
'& input::placeholder': {
101+
fontSize: '0.8rem',
102+
},
103+
}}
104+
/>
105+
{buttonVisible && (
106+
<Button
107+
variant="contained"
108+
size="medium"
109+
onClick={onButtonClick}
110+
sx={{
111+
backgroundColor: '#ffc400', // 노란색
112+
color: 'white',
113+
height: '40px',
114+
minWidth: '100px',
115+
fontWeight: '600',
116+
textTransform: 'none',
117+
borderRadius: '0.375rem',
118+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
119+
'&:hover': {
120+
backgroundColor: '#ff8146', // 주황색
121+
},
122+
'&:active': {
123+
backgroundColor: '#ff8146', // 주황색
124+
},
125+
}}
126+
>
127+
{buttonText}
128+
</Button>
129+
)}
130+
</div>
131+
</div>
132+
);
133+
}
134+
135+
export default CustomInput;

src/components/common/InputField.js

-57
This file was deleted.

src/components/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Calendar from "./camp/Calendar";
22
import CampingCard from "./camp/CampingCard";
3-
import InputField from "./common/InputField";
3+
import CustomInput from "./common/CustomInputField";
44
import LoadMoreButton from "./common/LoadMoreButton";
55
import ProfileCard from "./common/ProfileCard";
66
import ScrollToTopFab from "./common/ScrollToTopFab";
@@ -30,7 +30,7 @@ import NoResultsFound from "./main/NoResultsFound";
3030
export {
3131
Calendar,
3232
CampingCard,
33-
InputField,
33+
CustomInput,
3434
LoadMoreButton,
3535
ProfileCard,
3636
ScrollToTopFab,

0 commit comments

Comments
 (0)