This commit is contained in:
@ -10,6 +10,7 @@ import PostCreate from "./pages/PostCreate";
|
||||
import PostEdit from "./pages/PostEdit";
|
||||
import PostCategory from "./pages/PostCategory";
|
||||
import Login from "./pages/Login";
|
||||
import Register from './pages/Register';
|
||||
import Profile from "./pages/Profile";
|
||||
import { AuthProvider } from "./context/AuthContext";
|
||||
|
||||
@ -29,6 +30,7 @@ function App() {
|
||||
<Route path="/posts/:id/edit" element={<PostEdit />} />
|
||||
<Route path="/post/:category" element={<PostCategory />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
@ -6,18 +6,10 @@ const Footer = () => {
|
||||
return (
|
||||
<footer className="bg-gray-900 text-white mt-auto">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">TechEdu</h3>
|
||||
<p className="text-gray-400">최고의 IT 교육 플랫폼으로 당신의 커리어를 성장시키세요.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4">Quick Links</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><button onClick={() => navigate('/')} className="text-gray-400 hover:text-white">Home</button></li>
|
||||
<li><button onClick={() => navigate('/about')} className="text-gray-400 hover:text-white">About</button></li>
|
||||
<li><button onClick={() => navigate('/board')} className="text-gray-400 hover:text-white">Board</button></li>
|
||||
</ul>
|
||||
<h3 className="text-xl font-bold mb-4">System Management Portal</h3>
|
||||
<p className="text-gray-400">시스템 관리 포탈입니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4">Categories</h4>
|
||||
@ -30,14 +22,14 @@ const Footer = () => {
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4">Contact</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li className="flex items-center"><i className="fas fa-envelope mr-2"></i> contact@techedu.com</li>
|
||||
<li className="flex items-center"><i className="fas fa-phone mr-2"></i> 02-1234-5678</li>
|
||||
<li className="flex items-center"><i className="fas fa-map-marker-alt mr-2"></i> 서울특별시 강남구 테헤란로 123</li>
|
||||
<li className="flex items-center"><i className="fas fa-envelope mr-2"></i> icurfer@gmail.com</li>
|
||||
<li className="flex items-center"><i className="fas fa-phone mr-2"></i> -</li>
|
||||
<li className="flex items-center"><i className="fas fa-map-marker-alt mr-2"></i> -</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© 2025 TechEdu. All rights reserved.</p>
|
||||
<p> Copyright © icurfer 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,24 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
|
||||
const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isLoggedIn, logout } = useAuth();
|
||||
const { isLoggedIn, logout, user } = useAuth();
|
||||
|
||||
const menuItems = ['home', 'about', 'posts', 'paas', 'infra'];
|
||||
const menuItems = ["home", "about", "posts", "paas", "infra"];
|
||||
|
||||
return (
|
||||
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link to="/" className="text-2xl font-bold text-[#3B82F6]">Demo</Link>
|
||||
<Link to="/" className="text-2xl font-bold text-[#3B82F6]">
|
||||
Dev
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{menuItems.map(item => (
|
||||
{menuItems.map((item) => (
|
||||
<Link
|
||||
key={item}
|
||||
to={item === 'home' ? '/' : `/${item}`}
|
||||
to={item === "home" ? "/" : `/${item}`}
|
||||
className="text-gray-600 hover:text-[#D4AF37]"
|
||||
>
|
||||
{item.charAt(0).toUpperCase() + item.slice(1)}
|
||||
@ -29,14 +31,15 @@ const Navbar = () => {
|
||||
<>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="text-gray-600 hover:text-[#3B82F6]"
|
||||
// className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-[#8b0000]"
|
||||
className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
내 정보
|
||||
내 정보 {user?.grade && `(${user.grade})`}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
navigate("/login");
|
||||
}}
|
||||
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
@ -46,12 +49,20 @@ const Navbar = () => {
|
||||
)}
|
||||
|
||||
{!isLoggedIn && (
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
<Link
|
||||
to="/register"
|
||||
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
회원가입
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,30 +1,39 @@
|
||||
// src/context/AuthContext.js
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { decodeToken } from '../utils/jwt'; // ✅ JWT 디코딩 유틸 사용
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [user, setUser] = useState(null); // ✅ 추가
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('access');
|
||||
setIsLoggedIn(!!token);
|
||||
if (token) {
|
||||
const decoded = decodeToken(token); // payload 추출
|
||||
setUser(decoded);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = (tokenObj) => {
|
||||
localStorage.setItem('access', tokenObj.access);
|
||||
localStorage.setItem('refresh', tokenObj.refresh);
|
||||
const decoded = decodeToken(tokenObj.access);
|
||||
setUser(decoded); // ✅ 로그인 시 user 저장
|
||||
setIsLoggedIn(true);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setUser(null); // ✅ 로그아웃 시 제거
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isLoggedIn, login, logout }}>
|
||||
<AuthContext.Provider value={{ isLoggedIn, login, logout, user }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
@ -10,11 +10,11 @@ const About = () => {
|
||||
animation: false,
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '프로그래밍', max: 100 },
|
||||
{ name: '클라우드', max: 100 },
|
||||
{ name: '인공지능', max: 100 },
|
||||
{ name: '데이터 분석', max: 100 },
|
||||
{ name: '보안', max: 100 }
|
||||
{ name: '리눅스', max: 100 },
|
||||
{ name: '데브옵스', max: 100 },
|
||||
{ name: 'CI/CD', max: 100 },
|
||||
{ name: '기타', max: 100 }
|
||||
]
|
||||
},
|
||||
series: [{
|
||||
@ -35,27 +35,25 @@ const About = () => {
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold mb-6">About Us</h2>
|
||||
<p className="text-xl text-gray-600">최고의 IT 교육 플랫폼을 만들어갑니다</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-6">우리의 비전</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
실무 중심의 IT 교육을 통해 더 많은 사람들이 디지털 시대의 전문가로
|
||||
성장할 수 있도록 돕고, 글로벌 IT 인재 양성의 허브가 되고자 합니다.
|
||||
엔지니어에게 필요한 관리 포탈을 만듭니다.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-6">핵심 가치</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-center">
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>실무 중심 교육</span>
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>시스템 엔지니어를 위한 관리 도구</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>최신 기술 트렌드</span>
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>커뮤니티 중심</span>
|
||||
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -67,9 +65,9 @@ const About = () => {
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold mb-6">Contact Us</h3>
|
||||
<div className="space-y-4">
|
||||
<p><i className="fas fa-envelope mr-2"></i> contact@example.com</p>
|
||||
<p><i className="fas fa-phone mr-2"></i> 02-1234-5678</p>
|
||||
<p><i className="fas fa-map-marker-alt mr-2"></i> 서울특별시 강남구 테헤란로 123</p>
|
||||
<p><i className="fas fa-envelope mr-2"></i> icurfer@gmail.com</p>
|
||||
<p><i className="fas fa-phone mr-2"></i> - </p>
|
||||
<p><i className="fas fa-map-marker-alt mr-2"></i> - </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,9 +25,9 @@ const Home = () => {
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/70 to-transparent">
|
||||
<div className="container mx-auto px-4 h-full flex items-center">
|
||||
<div className="max-w-2xl text-white">
|
||||
<h1 className="text-5xl font-bold mb-6">Demo 사이트 입니다.</h1>
|
||||
<h1 className="text-5xl font-bold mb-6">System Management Portal</h1>
|
||||
<p className="text-xl mb-8">데모 사이트 입니다.</p>
|
||||
<button className="bg-[#3B82F6] text-white px-8 py-3 rounded-lg">demo</button>
|
||||
<button className="bg-[#3B82F6] text-white px-8 py-3 rounded-lg">개발 중 입니다.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,55 +1,66 @@
|
||||
// ./src/pages/Login.js
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import authApi from '../api/authApi';
|
||||
import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import authApi from "../api/authApi";
|
||||
import { useAuth } from "../context/AuthContext"; // ✅ 추가: AuthContext 사용
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth(); // ✅ 로그인 상태 업데이트 함수
|
||||
const { login } = useAuth(); // ✅ 로그인 상태 업데이트 함수
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const res = await authApi.post('/api/auth/login/', { email, password });
|
||||
const res = await authApi.post("/api/auth/login/", { email, password });
|
||||
|
||||
// ✅ localStorage 저장 + 전역 상태 갱신
|
||||
login(res.data); // ← access, refresh 포함된 객체
|
||||
login(res.data); // ← access, refresh 포함된 객체
|
||||
|
||||
alert('로그인 성공');
|
||||
navigate('/');
|
||||
alert("로그인 성공");
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.detail || '서버 오류';
|
||||
alert('로그인 실패: ' + message);
|
||||
const message = err.response?.data?.detail || "서버 오류";
|
||||
alert("로그인 실패: " + message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="bg-white p-8 rounded shadow-md w-full max-w-md">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center text-[#3B82F6]">로그인</h2>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="이메일"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded mb-4"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="비밀번호"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded mb-6"
|
||||
/>
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
<h2 className="text-2xl font-bold mb-6 text-center text-[#3B82F6]">
|
||||
로그인
|
||||
</button>
|
||||
</h2>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault(); // form 기본 동작 막기
|
||||
handleLogin(); // 로그인 실행
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="이메일"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded mb-4"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="비밀번호"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded mb-6"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
로그인
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
95
src/pages/Register.js
Normal file
95
src/pages/Register.js
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import authApi from '../api/authApi';
|
||||
|
||||
const Register = () => {
|
||||
const navigate = useNavigate();
|
||||
const [form, setForm] = useState({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
alert('비밀번호가 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await authApi.post('/api/auth/register/', {
|
||||
email: form.email,
|
||||
name: form.name,
|
||||
password: form.password
|
||||
});
|
||||
alert('회원가입 성공! 로그인 해주세요.');
|
||||
navigate('/login');
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.email?.[0] || err.response?.data?.detail || '회원가입 실패';
|
||||
alert(msg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-white p-8 rounded shadow-md w-full max-w-md"
|
||||
>
|
||||
<h2 className="text-2xl font-bold mb-6 text-center text-[#3B82F6]">회원가입</h2>
|
||||
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
placeholder="이메일"
|
||||
className="w-full px-4 py-2 border rounded mb-4"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
placeholder="이름"
|
||||
className="w-full px-4 py-2 border rounded mb-4"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
placeholder="비밀번호"
|
||||
className="w-full px-4 py-2 border rounded mb-4"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={form.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="비밀번호 확인"
|
||||
className="w-full px-4 py-2 border rounded mb-6"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
가입하기
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
10
src/utils/jwt.js
Normal file
10
src/utils/jwt.js
Normal file
@ -0,0 +1,10 @@
|
||||
// src/utils/jwt.js
|
||||
export function decodeToken(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
return JSON.parse(atob(base64));
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user