회원가입 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m36s

This commit is contained in:
2025-05-18 01:58:01 +09:00
parent c319f9ba78
commit 921359ad71
10 changed files with 211 additions and 83 deletions

View File

@ -10,6 +10,7 @@ import PostCreate from "./pages/PostCreate";
import PostEdit from "./pages/PostEdit"; import PostEdit from "./pages/PostEdit";
import PostCategory from "./pages/PostCategory"; import PostCategory from "./pages/PostCategory";
import Login from "./pages/Login"; import Login from "./pages/Login";
import Register from './pages/Register';
import Profile from "./pages/Profile"; import Profile from "./pages/Profile";
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider } from "./context/AuthContext";
@ -29,6 +30,7 @@ function App() {
<Route path="/posts/:id/edit" element={<PostEdit />} /> <Route path="/posts/:id/edit" element={<PostEdit />} />
<Route path="/post/:category" element={<PostCategory />} /> <Route path="/post/:category" element={<PostCategory />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
</Routes> </Routes>
</main> </main>

View File

@ -6,18 +6,10 @@ const Footer = () => {
return ( return (
<footer className="bg-gray-900 text-white mt-auto"> <footer className="bg-gray-900 text-white mt-auto">
<div className="container mx-auto px-4 py-12"> <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> <div>
<h3 className="text-xl font-bold mb-4">TechEdu</h3> <h3 className="text-xl font-bold mb-4">System Management Portal</h3>
<p className="text-gray-400">최고의 IT 교육 플랫폼으로 당신의 커리어를 성장시키세요.</p> <p className="text-gray-400">시스템 관리 포탈입니다.</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>
</div> </div>
<div> <div>
<h4 className="text-lg font-semibold mb-4">Categories</h4> <h4 className="text-lg font-semibold mb-4">Categories</h4>
@ -30,14 +22,14 @@ const Footer = () => {
<div> <div>
<h4 className="text-lg font-semibold mb-4">Contact</h4> <h4 className="text-lg font-semibold mb-4">Contact</h4>
<ul className="space-y-2 text-gray-400"> <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-envelope mr-2"></i> icurfer@gmail.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-phone mr-2"></i> -</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-map-marker-alt mr-2"></i> -</li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400"> <div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>&copy; 2025 TechEdu. All rights reserved.</p> <p> Copyright &copy; icurfer 2025</p>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -1,24 +1,26 @@
import React from 'react'; import React from "react";
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from "react-router-dom";
import { useAuth } from '../context/AuthContext'; import { useAuth } from "../context/AuthContext";
const Navbar = () => { const Navbar = () => {
const navigate = useNavigate(); 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 ( return (
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm"> <nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16"> <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"> <div className="hidden md:flex items-center space-x-8">
{menuItems.map(item => ( {menuItems.map((item) => (
<Link <Link
key={item} key={item}
to={item === 'home' ? '/' : `/${item}`} to={item === "home" ? "/" : `/${item}`}
className="text-gray-600 hover:text-[#D4AF37]" className="text-gray-600 hover:text-[#D4AF37]"
> >
{item.charAt(0).toUpperCase() + item.slice(1)} {item.charAt(0).toUpperCase() + item.slice(1)}
@ -29,14 +31,15 @@ const Navbar = () => {
<> <>
<Link <Link
to="/profile" 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> </Link>
<button <button
onClick={() => { onClick={() => {
logout(); logout();
navigate('/login'); navigate("/login");
}} }}
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300" className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
> >
@ -46,12 +49,20 @@ const Navbar = () => {
)} )}
{!isLoggedIn && ( {!isLoggedIn && (
<>
<Link <Link
to="/login" to="/login"
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700" className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700"
> >
로그인 로그인
</Link> </Link>
<Link
to="/register"
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
>
회원가입
</Link>
</>
)} )}
</div> </div>
</div> </div>

View File

@ -1,30 +1,39 @@
// src/context/AuthContext.js // src/context/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react'; import { createContext, useContext, useState, useEffect } from 'react';
import { decodeToken } from '../utils/jwt'; // ✅ JWT 디코딩 유틸 사용
const AuthContext = createContext(); const AuthContext = createContext();
export const AuthProvider = ({ children }) => { export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState(null); // ✅ 추가
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('access'); const token = localStorage.getItem('access');
setIsLoggedIn(!!token); if (token) {
const decoded = decodeToken(token); // payload 추출
setUser(decoded);
setIsLoggedIn(true);
}
}, []); }, []);
const login = (tokenObj) => { const login = (tokenObj) => {
localStorage.setItem('access', tokenObj.access); localStorage.setItem('access', tokenObj.access);
localStorage.setItem('refresh', tokenObj.refresh); localStorage.setItem('refresh', tokenObj.refresh);
const decoded = decodeToken(tokenObj.access);
setUser(decoded); // ✅ 로그인 시 user 저장
setIsLoggedIn(true); setIsLoggedIn(true);
}; };
const logout = () => { const logout = () => {
localStorage.removeItem('access'); localStorage.removeItem('access');
localStorage.removeItem('refresh'); localStorage.removeItem('refresh');
setUser(null); // ✅ 로그아웃 시 제거
setIsLoggedIn(false); setIsLoggedIn(false);
}; };
return ( return (
<AuthContext.Provider value={{ isLoggedIn, login, logout }}> <AuthContext.Provider value={{ isLoggedIn, login, logout, user }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -10,11 +10,11 @@ const About = () => {
animation: false, animation: false,
radar: { radar: {
indicator: [ indicator: [
{ name: '프로그래밍', max: 100 },
{ 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: [{ series: [{
@ -35,27 +35,25 @@ const About = () => {
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-6">About Us</h2> <h2 className="text-4xl font-bold mb-6">About Us</h2>
<p className="text-xl text-gray-600">최고의 IT 교육 플랫폼을 만들어갑니다</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20"> <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
<div> <div>
<h3 className="text-2xl font-bold mb-6">우리의 비전</h3> <h3 className="text-2xl font-bold mb-6">우리의 비전</h3>
<p className="text-gray-600 leading-relaxed"> <p className="text-gray-600 leading-relaxed">
실무 중심의 IT 교육 통해 많은 사람들이 디지털 시대의 전문가로 엔지니어에게 필요한 관리 포탈 만듭니다.
성장할 있도록 돕고, 글로벌 IT 인재 양성의 허브가 되고자 합니다.
</p> </p>
</div> </div>
<div> <div>
<h3 className="text-2xl font-bold mb-6">핵심 가치</h3> <h3 className="text-2xl font-bold mb-6">핵심 가치</h3>
<ul className="space-y-4"> <ul className="space-y-4">
<li className="flex items-center"> <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>
<li className="flex items-center"> <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>
<li className="flex items-center"> <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>
</ul> </ul>
</div> </div>
@ -67,9 +65,9 @@ const About = () => {
<div className="text-center"> <div className="text-center">
<h3 className="text-2xl font-bold mb-6">Contact Us</h3> <h3 className="text-2xl font-bold mb-6">Contact Us</h3>
<div className="space-y-4"> <div className="space-y-4">
<p><i className="fas fa-envelope mr-2"></i> contact@example.com</p> <p><i className="fas fa-envelope mr-2"></i> icurfer@gmail.com</p>
<p><i className="fas fa-phone mr-2"></i> 02-1234-5678</p> <p><i className="fas fa-phone mr-2"></i> - </p>
<p><i className="fas fa-map-marker-alt mr-2"></i> 123</p> <p><i className="fas fa-map-marker-alt mr-2"></i> - </p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,9 +25,9 @@ const Home = () => {
<div className="absolute inset-0 bg-gradient-to-r from-black/70 to-transparent"> <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="container mx-auto px-4 h-full flex items-center">
<div className="max-w-2xl text-white"> <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> <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> </div>
</div> </div>

View File

@ -1,41 +1,50 @@
// ./src/pages/Login.js // ./src/pages/Login.js
import React, { useState } from 'react'; import React, { useState } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import authApi from '../api/authApi'; import authApi from "../api/authApi";
import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용 import { useAuth } from "../context/AuthContext"; // ✅ 추가: AuthContext 사용
const Login = () => { const Login = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { login } = useAuth(); // ✅ 로그인 상태 업데이트 함수 const { login } = useAuth(); // ✅ 로그인 상태 업데이트 함수
const [email, setEmail] = useState(''); const [email, setEmail] = useState("");
const [password, setPassword] = useState(''); const [password, setPassword] = useState("");
const handleLogin = async () => { const handleLogin = async () => {
try { try {
const res = await authApi.post('/api/auth/login/', { email, password }); const res = await authApi.post("/api/auth/login/", { email, password });
// ✅ localStorage 저장 + 전역 상태 갱신 // ✅ localStorage 저장 + 전역 상태 갱신
login(res.data); // ← access, refresh 포함된 객체 login(res.data); // ← access, refresh 포함된 객체
alert('로그인 성공'); alert("로그인 성공");
navigate('/'); navigate("/");
} catch (err) { } catch (err) {
const message = err.response?.data?.detail || '서버 오류'; const message = err.response?.data?.detail || "서버 오류";
alert('로그인 실패: ' + message); alert("로그인 실패: " + message);
} }
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-100"> <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"> <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> <h2 className="text-2xl font-bold mb-6 text-center text-[#3B82F6]">
로그인
</h2>
<form
onSubmit={(e) => {
e.preventDefault(); // form 기본 동작 막기
handleLogin(); // 로그인 실행
}}
>
<input <input
type="email" type="email"
placeholder="이메일" placeholder="이메일"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border rounded mb-4" className="w-full px-4 py-2 border rounded mb-4"
required
/> />
<input <input
type="password" type="password"
@ -43,13 +52,15 @@ const Login = () => {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border rounded mb-6" className="w-full px-4 py-2 border rounded mb-6"
required
/> />
<button <button
onClick={handleLogin} type="submit"
className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700" className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700"
> >
로그인 로그인
</button> </button>
</form>
</div> </div>
</div> </div>
); );

95
src/pages/Register.js Normal file
View 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
View 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;
}
}

View File

@ -1 +1 @@
0.0.8 0.0.9