diff --git a/package.json b/package.json
index 0c47ad7..cea93e6 100644
--- a/package.json
+++ b/package.json
@@ -33,9 +33,11 @@
"@types/prop-types": "^15.7.15",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+ "axios": "^1.13.2",
"chart.js": "^4.5.1",
"classnames": "^2.5.1",
"core-js": "^3.47.0",
+ "jwt-decode": "^4.0.0",
"prop-types": "^15.8.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
diff --git a/src/_nav.tsx b/src/_nav.tsx
index 43fd900..1809cc9 100644
--- a/src/_nav.tsx
+++ b/src/_nav.tsx
@@ -76,24 +76,6 @@ const _nav = [
},
],
},
- {
- component: CNavItem,
- name: 'Demo',
- href: 'https://coreui.io/demos/react/5.5/free/?theme=light#/dashboard',
- icon: ,
- },
- {
- component: CNavItem,
- name: 'Docs',
- href: 'https://coreui.io/react/docs/templates/installation/',
- icon: ,
- },
- {
- component: CNavItem,
- name: 'Src',
- href: 'https://github.com/coreui/coreui-free-react-admin-template',
- icon: ,
- },
]
export default _nav
diff --git a/src/axios/authService.ts b/src/axios/authService.ts
new file mode 100644
index 0000000..7a7c3e7
--- /dev/null
+++ b/src/axios/authService.ts
@@ -0,0 +1,83 @@
+import axios from './axios';
+import { jwtDecode } from 'jwt-decode';
+
+export const getAccessTokenFromCookie = (): string | null => {
+ const name = 'access_token';
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
+ return null;
+};
+
+export interface LoginCredentials {
+ memberId: string;
+ password: string;
+}
+
+export interface RegisterData {
+ username: string;
+ email: string;
+ password: string;
+}
+
+export interface AuthResponse {
+ resultCode: string;
+ resultMessage: string;
+ resultData: string;
+}
+
+export interface DecodedToken {
+ encryptedPayload: string;
+ memberId: string;
+ memberName: string;
+ expiration: number;
+}
+
+// 로그인 API
+export const login = async (credentials: LoginCredentials): Promise => {
+ const response = await axios.post('/auth/login', credentials);
+ return response.data;
+};
+
+// 회원가입 API
+export const register = async (data: RegisterData): Promise<{ message: string }> => {
+ const response = await axios.post<{ message: string }>('/auth/register', data);
+ return response.data;
+};
+
+// 로그아웃 API
+export const logout = async (): Promise => {
+ await axios.post('/auth/logout');
+ //localStorage.removeItem('access_token');
+ //localStorage.removeItem('refresh_token');
+ document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
+ document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
+};
+
+// 토큰이 유효한지 확인
+export const isTokenValid = (token: string): boolean => {
+ try {
+ const decoded = jwtDecode(token);
+ const currentTime = Date.now() / 1000;
+
+ return decoded.expiration > currentTime;
+ } catch (error) {
+ return false;
+ }
+};
+
+// 토큰에서 사용자 정보 추출
+export const getUserFromToken = (token: string): DecodedToken | null => {
+ try {
+ const decoded = jwtDecode(token);
+ return decoded;
+ } catch (error) {
+ return null;
+ }
+};
+
+// 현재 사용자 정보 가져오기
+export const getCurrentUser = async () => {
+ const response = await axios.get('/auth/me');
+ return response.data;
+};
\ No newline at end of file
diff --git a/src/axios/axios.ts b/src/axios/axios.ts
new file mode 100644
index 0000000..e077bfb
--- /dev/null
+++ b/src/axios/axios.ts
@@ -0,0 +1,26 @@
+import axios from 'axios';
+
+const API_URL = '/api/v1/';
+
+// Axios 인스턴스 생성
+const instance = axios.create({
+ baseURL: API_URL,
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+});
+
+// 요청 인터셉터
+instance.interceptors.request.use(
+ (config) => {
+ const accessToken = localStorage.getItem('access_token');
+ if (accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+);
+
+export default instance;
\ No newline at end of file
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx
index 40db829..16d83ec 100644
--- a/src/components/AppHeader.tsx
+++ b/src/components/AppHeader.tsx
@@ -28,6 +28,7 @@ import {
import { AppBreadcrumb } from './index'
import { AppHeaderDropdown } from './header/index'
+import { useAuth } from 'src/hooks/useAuth'
const AppHeader = () => {
const headerRef = useRef(null)
@@ -35,6 +36,8 @@ const AppHeader = () => {
const dispatch = useDispatch()
const sidebarShow = useSelector((state: RootState) => state.sidebarShow)
+ const { state } = useAuth()
+ const { isAuthenticated, member } = state
useEffect(() => {
const handleScroll = () => {
@@ -61,6 +64,11 @@ const AppHeader = () => {
+ {isAuthenticated && member && (
+
+ {member.memberId}
+
+ )}
diff --git a/src/components/header/AppHeaderDropdown.tsx b/src/components/header/AppHeaderDropdown.tsx
index 30c0df8..9021a10 100644
--- a/src/components/header/AppHeaderDropdown.tsx
+++ b/src/components/header/AppHeaderDropdown.tsx
@@ -27,10 +27,10 @@ import avatar8 from './../../assets/images/avatars/8.jpg'
const AppHeaderDropdown = () => {
return (
-
+
-
+
Account
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..440f223
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,144 @@
+import React, { createContext, useReducer, useEffect } from 'react';
+
+import { getCurrentUser, isTokenValid } from 'src/axios/authService';
+
+// 사용자 타입 정의
+export interface Member {
+ memberId: string;
+ memberName: string;
+}
+
+// 인증 상태 타입 정의
+interface AuthState {
+ isAuthenticated: boolean;
+ member: Member | null;
+ loading: boolean;
+ error: string | null;
+}
+
+// 액션 타입 정의
+type AuthAction =
+ | { type: 'LOGIN_SUCCESS'; payload: { member: Member; token: string } }
+ | { type: 'LOGOUT' }
+ | { type: 'AUTH_ERROR'; payload: string }
+ | { type: 'CLEAR_ERROR' }
+ | { type: 'SET_LOADING' }
+ | { type: 'MEMBER_LOADED'; payload: Member };
+
+// 초기 상태
+const initialState: AuthState = {
+ isAuthenticated: false,
+ member: null,
+ loading: true,
+ error: null
+};
+
+// Context 생성
+export const AuthContext = createContext<{
+ state: AuthState;
+ dispatch: React.Dispatch;
+ login: (token: string, member: Member) => void;
+ logout: () => void;
+}>({
+ state: initialState,
+ dispatch: () => null,
+ login: () => null,
+ logout: () => null
+});
+
+// 리듀서 함수
+const authReducer = (state: AuthState, action: AuthAction): AuthState => {
+ switch (action.type) {
+ case 'LOGIN_SUCCESS':
+ localStorage.setItem('accessToken', action.payload.token);
+ return {
+ ...state,
+ isAuthenticated: true,
+ member: action.payload.member,
+ loading: false,
+ error: null
+ };
+ case 'LOGOUT':
+ localStorage.removeItem('accessToken');
+ return {
+ ...state,
+ isAuthenticated: false,
+ member: null,
+ loading: false,
+ error: null
+ };
+ case 'AUTH_ERROR':
+ localStorage.removeItem('accessToken');
+ return {
+ ...state,
+ isAuthenticated: false,
+ member: null,
+ loading: false,
+ error: action.payload
+ };
+ case 'CLEAR_ERROR':
+ return {
+ ...state,
+ error: null
+ };
+ case 'SET_LOADING':
+ return {
+ ...state,
+ loading: true
+ };
+ case 'MEMBER_LOADED':
+ return {
+ ...state,
+ isAuthenticated: true,
+ member: action.payload,
+ loading: false
+ };
+ default:
+ return state;
+ }
+};
+
+// Provider 컴포넌트
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [state, dispatch] = useReducer(authReducer, initialState);
+
+ // 로그인 함수
+ const login = (token: string, member: Member) => {
+ dispatch({
+ type: 'LOGIN_SUCCESS',
+ payload: { token, member }
+ });
+ };
+
+ // 로그아웃 함수
+ const logout = () => {
+ dispatch({ type: 'LOGOUT' });
+ };
+
+ // 초기 인증 상태 확인
+ useEffect(() => {
+ const loadUser = async () => {
+ const token = localStorage.getItem('accessToken');
+
+ if (!token || !isTokenValid(token)) {
+ dispatch({ type: 'LOGOUT' });
+ return;
+ }
+
+ try {
+ const member = await getCurrentUser();
+ dispatch({ type: 'MEMBER_LOADED', payload: member });
+ } catch (error) {
+ dispatch({ type: 'AUTH_ERROR', payload: 'Authentication failed' });
+ }
+ };
+
+ loadUser();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
new file mode 100644
index 0000000..efca210
--- /dev/null
+++ b/src/hooks/useAuth.ts
@@ -0,0 +1,12 @@
+import { useContext } from 'react';
+import { AuthContext } from '../context/AuthContext';
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+
+ return context;
+};
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 441af3a..14542f6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -5,9 +5,12 @@ import 'core-js'
import App from './App'
import store from './store'
+import { AuthProvider } from './context/AuthContext'
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
)
diff --git a/src/views/pages/login/Login.tsx b/src/views/pages/login/Login.tsx
index 1b2ee0b..36de7ab 100644
--- a/src/views/pages/login/Login.tsx
+++ b/src/views/pages/login/Login.tsx
@@ -1,5 +1,5 @@
-import React from 'react'
-import { Link } from 'react-router-dom'
+import React, { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
import {
CButton,
CCard,
@@ -15,8 +15,53 @@ import {
} from '@coreui/react'
import CIcon from '@coreui/icons-react'
import { cilLockLocked, cilUser } from '@coreui/icons'
+import { login as apiLogin, DecodedToken, getAccessTokenFromCookie } from 'src/axios/authService';
+import { useAuth } from 'src/hooks/useAuth';
+import { jwtDecode } from 'jwt-decode';
const Login = () => {
+ const [memberId, setMemberId] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ const response = await apiLogin({ memberId, password });
+
+ if (response.resultCode === '200') {
+ const accessToken = getAccessTokenFromCookie();
+
+ if (accessToken) {
+ const decodedToken = jwtDecode(accessToken);
+ const member = {
+ memberId: decodedToken.memberId,
+ memberName: decodedToken.memberName,
+ };
+
+ login(accessToken, member);
+ navigate('/dashboard');
+ } else {
+ setError('쿠키에서 accessToken을 찾을 수 없습니다.');
+ }
+ } else {
+ setError(response.resultMessage || '로그인에 실패했습니다.');
+ }
+ } catch (err: any) {
+ setError(err.response?.data?.message || '로그인 중 오류가 발생했습니다.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+
return (
@@ -25,14 +70,20 @@ const Login = () => {
-
+
Login
Sign In to your account
+ {error && {error}
}
-
+ setMemberId(e.target.value)}
+ />
@@ -42,12 +93,14 @@ const Login = () => {
type="password"
placeholder="Password"
autoComplete="current-password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
/>
-
- Login
+
+ {loading ? 'Logging in...' : 'Login'}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/tsconfig.json b/tsconfig.json
index c349a5e..f72cea1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,14 +9,6 @@
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
- // Style Options
- // "noImplicitReturns": true,
- // "noImplicitOverride": true,
- // "noUnusedLocals": true,
- // "noUnusedParameters": true,
- // "noFallthroughCasesInSwitch": true,
- // "noPropertyAccessFromIndexSignature": true,
- // Recommended Options
"verbatimModuleSyntax": false,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,