diff --git a/package-lock.json b/package-lock.json index 4cb05c9..f919b93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.8.4", "echarts": "^5.6.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -4923,6 +4924,32 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -13684,6 +13711,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/package.json b/package.json index ec03503..fb362be 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.8.4", "echarts": "^5.6.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/App.js b/src/App.js index 5983596..f75ba64 100644 --- a/src/App.js +++ b/src/App.js @@ -7,24 +7,27 @@ import About from './pages/About'; import Board from './pages/Board'; import PostCategory from './pages/PostCategory'; import Login from './pages/Login'; +import { AuthProvider } from './context/AuthContext'; function App() { return ( - - - - - - } /> - } /> - } /> - } /> - } /> - - - - - + + + + + + + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/src/api/api.js b/src/api/api.js new file mode 100644 index 0000000..0566d98 --- /dev/null +++ b/src/api/api.js @@ -0,0 +1,20 @@ +// src/api/api.js +import axios from 'axios'; + +const api = axios.create({ + baseURL: process.env.REACT_APP_API_URL, // ✅ 환경변수에서 읽어옴 + headers: { + 'Content-Type': 'application/json', + } +}); + +// 요청에 JWT 자동 포함 +api.interceptors.request.use(config => { + const token = localStorage.getItem('access'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +export default api; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index ddd6d9e..c9e3285 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -1,18 +1,22 @@ import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; // ✅ Context에서 로그인 상태 사용 const Navbar = () => { + const navigate = useNavigate(); + const { isLoggedIn, logout } = useAuth(); // ✅ 로그인 상태 + 로그아웃 함수 + const menuItems = ['home', 'about', 'board', 'etc']; const postCategories = ['developer', 'systemengineer', 'etc']; - const navigate = useNavigate(); return ( - TechEdu + Demo + - {menuItems.map((item) => ( + {menuItems.map(item => ( { - {/* ✅ 로그인 버튼 */} - - 로그인 - + {/* ✅ 로그인 상태에 따른 버튼 */} + {isLoggedIn ? ( + { + logout(); + navigate('/login'); + }} + className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300" + > + 로그아웃 + + ) : ( + + 로그인 + + )} diff --git a/src/components/ProtectedRoute.js b/src/components/ProtectedRoute.js new file mode 100644 index 0000000..aa65939 --- /dev/null +++ b/src/components/ProtectedRoute.js @@ -0,0 +1,11 @@ +// src/components/ProtectedRoute.js +import React from 'react'; +import { Navigate } from 'react-router-dom'; + +const ProtectedRoute = ({ children }) => { + const token = localStorage.getItem('access'); + + return token ? children : ; +}; + +export default ProtectedRoute; diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js new file mode 100644 index 0000000..33f2cb6 --- /dev/null +++ b/src/context/AuthContext.js @@ -0,0 +1,33 @@ +// src/context/AuthContext.js +import { createContext, useContext, useState, useEffect } from 'react'; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + const token = localStorage.getItem('access'); + setIsLoggedIn(!!token); + }, []); + + const login = (tokenObj) => { + localStorage.setItem('access', tokenObj.access); + localStorage.setItem('refresh', tokenObj.refresh); + setIsLoggedIn(true); + }; + + const logout = () => { + localStorage.removeItem('access'); + localStorage.removeItem('refresh'); + setIsLoggedIn(false); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/src/pages/Home.js b/src/pages/Home.js index a62c3f2..eaac193 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -16,9 +16,9 @@ const Home = () => { - IT 교육의 새로운 기준 - 실무 중심의 IT 교육 콘텐츠로 당신의 커리어를 성장시키세요 - 학습 시작하기 + Demo 사이트 입니다. + 데모 사이트 입니다. + demo diff --git a/src/pages/Login.js b/src/pages/Login.js index 46ca205..1f938d4 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -1,45 +1,47 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import api from '../api/api'; +import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용 const Login = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); const navigate = useNavigate(); + const { login } = useAuth(); // ✅ 로그인 상태 업데이트 함수 + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); const handleLogin = async () => { try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/auth/login/`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) + const res = await api.post('/api/auth/login/', { + email, + password }); - if (!response.ok) throw new Error('로그인 실패'); + // ✅ localStorage 저장 + 전역 상태 갱신 + login(res.data); // ← access, refresh 포함된 객체 - const data = await response.json(); - localStorage.setItem('access', data.access); - localStorage.setItem('refresh', data.refresh); - alert('로그인 성공!'); + alert('로그인 성공'); navigate('/'); - } catch (error) { - alert('로그인 실패: ' + error.message); + } catch (err) { + const message = err.response?.data?.detail || '서버 오류'; + alert('로그인 실패: ' + message); } }; return ( - - + + 로그인 setUsername(e.target.value)} + type="email" + placeholder="이메일" + value={email} + onChange={(e) => setEmail(e.target.value)} className="w-full px-4 py-2 border rounded mb-4" /> setPassword(e.target.value)} className="w-full px-4 py-2 border rounded mb-6"
실무 중심의 IT 교육 콘텐츠로 당신의 커리어를 성장시키세요
데모 사이트 입니다.