회원가입 기능 추가
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 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>

View File

@ -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>&copy; 2025 TechEdu. All rights reserved.</p>
<p> Copyright &copy; icurfer 2025</p>
</div>
</div>
</footer>

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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>

View File

@ -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
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