Skip to content
Open
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"react-hook-form": "^7.54.1",
"react-redux": "^9.1.2",
"react-router": "^7.0.2",
"redux-persist": "^6.0.0",
"styled-components": "^6.1.13"
},
"devDependencies": {
Expand All @@ -39,6 +40,7 @@
"@types/jwt-decode": "^3.1.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/redux-persist": "^4.3.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
Expand Down
32 changes: 32 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider, createBrowserRouter } from 'react-router';
import router from '@/routes/router';
import { Provider } from 'react-redux'; // Redux Provider 임포트
import { store } from '@/store/store'; // 설정된 Redux 스토어 임포트
import GlobalStyle from './styles/GlobalStyle';
import { PersistGate } from 'redux-persist/integration/react';
import { persistor, store } from './store/store';

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -19,10 +20,12 @@ const App = () => {
const appRouter = createBrowserRouter(router);
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<GlobalStyle />
<RouterProvider router={appRouter} />
</QueryClientProvider>
<PersistGate loading={null} persistor={persistor}>
<QueryClientProvider client={queryClient}>
<GlobalStyle />
<RouterProvider router={appRouter} />
</QueryClientProvider>
</PersistGate>
</Provider>
);
};
Expand Down
63 changes: 36 additions & 27 deletions src/apis/http.api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { getToken, removeToken } from '@/utils/token'; // 토큰 유틸리티 함수 import
import { logout } from '@/store/slices/authSlice';
import { store } from '@/store/store';
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosHeaders,
AxiosRequestHeaders,
} from 'axios';

const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3333';
const DEFAULT_TIMEOUT = 30000; // 요청 제한 시간

// 토큰 관리 함수
function getToken(): string | null {
return localStorage.getItem('token');
}

function setToken(token: string) {
localStorage.setItem('token', token);
}

function removeToken() {
localStorage.removeItem('token');
}

// Axios 인스턴스 생성 함수
export const createClient = (config?: AxiosRequestConfig): AxiosInstance => {
const axiosInstance = axios.create({
Expand All @@ -21,17 +36,24 @@ export const createClient = (config?: AxiosRequestConfig): AxiosInstance => {
// 요청 인터셉터: Authorization 헤더 동적 설정
axiosInstance.interceptors.request.use(
(config) => {
const accessToken = getToken();
const accessToken = getToken(); // 여기서 getToken()을 호출하여 실제로 사용
if (accessToken) {
if (config.headers && typeof config.headers.set === 'function') {
// AxiosHeaders 객체 처리
config.headers.set('Authorization', `Bearer ${accessToken}`);
if (
config.headers &&
'set' in config.headers &&
typeof (config.headers as AxiosHeaders).set === 'function'
) {
Comment on lines +41 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (
config.headers &&
'set' in config.headers &&
typeof (config.headers as AxiosHeaders).set === 'function'
) {
if (typeof config?.headers?.set === 'function') {

이런 식으로 간단하게 표기해볼 수도 있겠네요 🤔

// AxiosHeaders 타입으로 헤더를 다루는 경우
(config.headers as AxiosHeaders).set(
'Authorization',
`Bearer ${accessToken}`
);
} else {
// 일반 객체 초기화
// 일반 객체로 헤더를 다루는 경우
config.headers = {
...config.headers, // 기존 헤더 유지
...config.headers,
Authorization: `Bearer ${accessToken}`,
} as any;
} as AxiosRequestHeaders;
}
}
console.log('Request Headers:', config.headers); // 디버깅용 로그
Expand All @@ -43,26 +65,13 @@ export const createClient = (config?: AxiosRequestConfig): AxiosInstance => {
}
);

// 응답 인터셉터: 401 상태 처리
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
console.warn('401 Unauthorized: Logging out user...');
removeToken(); // 토큰 삭제
store.dispatch(logout()); // Redux 상태 초기화
window.location.href = '/login'; // 로그인 페이지로 리다이렉트
return;
}
return Promise.reject(error);
}
);

return axiosInstance;
};

// 기본 Axios 인스턴스 생성
export const httpClient = createClient();

// 기본 Axios 인스턴스 내보내기
// 토큰 관련 함수 export
export { setToken, removeToken, getToken };

export default httpClient;
95 changes: 95 additions & 0 deletions src/components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// src/components/Header.tsx
import { useNavigate } from 'react-router';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@/store/rootReducer';
import { logout } from '@/hooks/userSlice';
import { removeToken } from '@/apis/http.api';
import styled from 'styled-components';

function AuthButton() {
const navigate = useNavigate();
const dispatch = useDispatch();
const isLoggedIn = useSelector((state: RootState) => state.user.isLoggedIn);

const handleLogin = () => {
// 로그인 페이지로 이동
navigate('/login');
};

const handleLogout = () => {
// Redux 상태 초기화
dispatch(logout());
// 토큰 제거
removeToken();
// 로그아웃 후 메인 페이지 이동(필요하다면)
navigate('/');
};

return isLoggedIn ? (
<LogoutBox onClick={handleLogout}>
<div className='group'>
<div className='overlap-group'>
<div className='text-wrapper'>로그아웃</div>
</div>
</div>
</LogoutBox>
) : (
<LoginBox onClick={handleLogin}>
<div className='group'>
<div className='overlap-group'>
<div className='text-wrapper'>로그인</div>
</div>
</div>
</LoginBox>
Comment on lines +29 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoginButton과 LogoutButton의 두 구조가 완전하게 동일하네요.
중복을 줄이려면 어떻게 하는 게 좋을까요?
styled component에 props를 전달하여 내부에서 스타일을 조정할 수도 있는데, 해당 방식을 사용해보는 건 어때요? 👀

);
}

export default AuthButton;

// Styled Components 동일
const ButtonBox = styled.div`
height: 52px;
width: 114px;
.group {
height: 100%;
position: relative;
}
.overlap-group {
border-radius: 30px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.text-wrapper {
color: #ffffff;
font-family: 'Pretendard-ExtraBold', Helvetica;
font-size: 15px;
line-height: 12px;
white-space: nowrap;
}
`;

const LoginBox = styled(ButtonBox)`
.overlap-group {
background-color: #32c040;
}
.overlap-group:hover {
background-color: #28a034;
}
`;

const LogoutBox = styled(ButtonBox)`
.overlap-group {
background-color: #9a9a9a;
}
.overlap-group:hover {
background-color: #7f7f7f;
}
`;
2 changes: 2 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import logo from '@/assets/logo.svg';
import Avatar from './ui/atoms/Avator';
import AuthButton from './AuthButton';

const Header = () => (
<HeaderContainer>
Expand All @@ -14,6 +15,7 @@ const Header = () => (
/>
</div>
</HeaderWrapper>
<AuthButton />
</HeaderContainer>
);

Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/atoms/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface LabelProps {

const StyledLabel = styled.label<LabelProps>`
position: absolute;
left: 1rem;
left: 2rem;
top: 50%;
transform: translateY(-50%);
color: #727272;
Expand Down
Loading