This commit is contained in:
@ -4,7 +4,8 @@ import Navbar from './components/Navbar';
|
||||
import Footer from './components/Footer';
|
||||
import Home from './pages/Home';
|
||||
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 Login from './pages/Login';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
@ -19,7 +20,8 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<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="/login" element={<Login />} />
|
||||
</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 navigate = useNavigate();
|
||||
const { isLoggedIn, logout } = useAuth(); // ✅ 로그인 상태 + 로그아웃 함수
|
||||
const { isLoggedIn, logout } = useAuth();
|
||||
|
||||
const menuItems = ['home', 'about', 'board', 'etc'];
|
||||
const postCategories = ['developer', 'systemengineer', 'etc'];
|
||||
// ✅ 'board' → 'posts'로 변경
|
||||
const menuItems = ['home', 'about', 'posts'];
|
||||
|
||||
return (
|
||||
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
|
||||
@ -26,25 +26,6 @@ const Navbar = () => {
|
||||
</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 ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../api/api';
|
||||
import authApi from '../api/authApi';
|
||||
import { useAuth } from '../context/AuthContext'; // ✅ 추가: AuthContext 사용
|
||||
|
||||
const Login = () => {
|
||||
@ -12,10 +12,7 @@ const Login = () => {
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const res = await api.post('/api/auth/login/', {
|
||||
email,
|
||||
password
|
||||
});
|
||||
const res = await authApi.post('/api/auth/login/', { email, password });
|
||||
|
||||
// ✅ localStorage 저장 + 전역 상태 갱신
|
||||
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