This commit is contained in:
@ -4,7 +4,8 @@ import Navbar from './components/Navbar';
|
|||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import About from './pages/About';
|
import About from './pages/About';
|
||||||
import Board from './pages/Board';
|
import PostList from './pages/PostList';
|
||||||
|
import PostCreate from './pages/PostCreate';
|
||||||
import PostCategory from './pages/PostCategory';
|
import PostCategory from './pages/PostCategory';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import { AuthProvider } from './context/AuthContext';
|
||||||
@ -19,7 +20,8 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/board" element={<Board />} />
|
<Route path="/posts" element={<PostList />} />
|
||||||
|
<Route path="/postCreate" element={<PostCreate />} />
|
||||||
<Route path="/post/:category" element={<PostCategory />} />
|
<Route path="/post/:category" element={<PostCategory />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
// 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;
|
|
20
src/api/authApi.js
Normal file
20
src/api/authApi.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// src/api/authApi.js
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const authApi = axios.create({
|
||||||
|
baseURL: process.env.REACT_APP_API_AUTH,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// JWT 자동 포함 (로그인 이후 /me 호출 등에 사용 가능)
|
||||||
|
authApi.interceptors.request.use(config => {
|
||||||
|
const token = localStorage.getItem('access');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authApi;
|
20
src/api/blogApi.js
Normal file
20
src/api/blogApi.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// src/api/blogApi.js
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const blogApi = axios.create({
|
||||||
|
baseURL: process.env.REACT_APP_API_BLOG,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 게시글 작성 시에도 JWT 필요하므로 interceptor 동일하게 구성
|
||||||
|
blogApi.interceptors.request.use(config => {
|
||||||
|
const token = localStorage.getItem('access');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default blogApi;
|
@ -4,10 +4,10 @@ import { useAuth } from '../context/AuthContext'; // ✅ Context에서 로그
|
|||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isLoggedIn, logout } = useAuth(); // ✅ 로그인 상태 + 로그아웃 함수
|
const { isLoggedIn, logout } = useAuth();
|
||||||
|
|
||||||
const menuItems = ['home', 'about', 'board', 'etc'];
|
// ✅ 'board' → 'posts'로 변경
|
||||||
const postCategories = ['developer', 'systemengineer', 'etc'];
|
const menuItems = ['home', 'about', 'posts'];
|
||||||
|
|
||||||
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">
|
||||||
@ -26,25 +26,6 @@ const Navbar = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* ▼ Post Dropdown */}
|
|
||||||
<div className="relative group">
|
|
||||||
<button className="text-gray-600 hover:text-[#D4AF37] flex items-center">
|
|
||||||
Post <i className="fas fa-chevron-down ml-1 text-xs"></i>
|
|
||||||
</button>
|
|
||||||
<div className="absolute hidden group-hover:block bg-white shadow-md mt-2 rounded-md py-2">
|
|
||||||
{postCategories.map(category => (
|
|
||||||
<button
|
|
||||||
key={category}
|
|
||||||
onClick={() => navigate(`/post/${category.toLowerCase()}`)}
|
|
||||||
className="block px-4 py-2 text-gray-600 hover:bg-gray-50 hover:text-[#D4AF37] w-full text-left"
|
|
||||||
>
|
|
||||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ✅ 로그인 상태에 따른 버튼 */}
|
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import api from '../api/api';
|
import authApi from '../api/authApi';
|
||||||
import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용
|
import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
@ -12,10 +12,7 @@ const Login = () => {
|
|||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.post('/api/auth/login/', {
|
const res = await authApi.post('/api/auth/login/', { email, password });
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ localStorage 저장 + 전역 상태 갱신
|
// ✅ localStorage 저장 + 전역 상태 갱신
|
||||||
login(res.data); // ← access, refresh 포함된 객체
|
login(res.data); // ← access, refresh 포함된 객체
|
||||||
|
46
src/pages/PostCreate.js
Normal file
46
src/pages/PostCreate.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import blogApi from '../api/blogApi';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const PostCreate = () => {
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await blogApi.post('/api/blog/create/', { title, content });
|
||||||
|
alert('게시글이 등록되었습니다.');
|
||||||
|
navigate('/posts');
|
||||||
|
} catch (err) {
|
||||||
|
alert('등록 실패: ' + (err.response?.data?.detail || err.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">게시글 작성</h1>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="제목"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 mb-4 border rounded"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="내용"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 mb-4 border rounded min-h-[150px]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="bg-[#3B82F6] text-white px-6 py-2 rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
작성 완료
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostCreate;
|
47
src/pages/PostList.js
Normal file
47
src/pages/PostList.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import blogApi from '../api/blogApi';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const PostList = () => {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
blogApi.get('/api/blog/posts/')
|
||||||
|
.then(res => setPosts(res.data))
|
||||||
|
.catch(err => console.error('게시글 목록 조회 실패:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-8">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">게시글 목록</h1>
|
||||||
|
|
||||||
|
{isLoggedIn && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/postCreate')}
|
||||||
|
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
게시물 등록
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{posts.map(post => (
|
||||||
|
<li key={post.id} className="p-4 border rounded shadow-sm bg-white">
|
||||||
|
<h2 className="text-xl font-semibold">{post.title}</h2>
|
||||||
|
<p className="text-gray-600">작성자: {post.author_name}</p>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
작성일: {new Date(post.created_at).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostList;
|
Reference in New Issue
Block a user