This commit is contained in:
@ -1,4 +1,5 @@
|
||||
REACT_APP_API_AUTH=http://127.0.0.1:8000
|
||||
REACT_APP_API_BLOG=http://127.0.0.1:8800
|
||||
REACT_APP_API_BOARD=http://127.0.0.1:8801
|
||||
REACT_APP_API_TODO=http://127.0.0.1:8880
|
||||
REACT_APP_API_ANSIBLE=http://127.0.0.1:8888
|
@ -1,4 +1,5 @@
|
||||
REACT_APP_API_AUTH=https://www.icurfer.com
|
||||
REACT_APP_API_BLOG=https://www.icurfer.com
|
||||
REACT_APP_API_BOARD=https://www.icurfer.com
|
||||
REACT_APP_API_TODO=https://www.icurfer.com
|
||||
REACT_APP_API_ANSIBLE=https://www.icurfer.com
|
@ -13,6 +13,7 @@ import Login from "./pages/Login";
|
||||
import Register from './pages/Register';
|
||||
import Profile from "./pages/Profile";
|
||||
import TaskPage from "./pages/TaskPage";
|
||||
import BoardsPage from "./pages/BoardsPage";
|
||||
import AnsiblePage from "./pages/Ansible";
|
||||
|
||||
import { AuthProvider } from "./context/AuthContext";
|
||||
@ -36,6 +37,7 @@ function App() {
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/tasks" element={<TaskPage />} />
|
||||
<Route path="/boards" element={<BoardsPage />} />
|
||||
<Route path="/ansible" element={<AnsiblePage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
38
src/api/boardApi.js
Normal file
38
src/api/boardApi.js
Normal file
@ -0,0 +1,38 @@
|
||||
// src/api/boardApi.js
|
||||
import axios from 'axios';
|
||||
import { attachAuthInterceptors } from './attachInterceptors';
|
||||
|
||||
const boardApi = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_BOARD, // ex) http://localhost:8801/api
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// JWT 인증 토큰 인터셉터 부착
|
||||
attachAuthInterceptors(boardApi);
|
||||
|
||||
// ✅ 게시판 목록 조회
|
||||
export const fetchBoards = () => boardApi.get('/api/boards/');
|
||||
|
||||
// ✅ 특정 게시판의 게시글 목록 조회
|
||||
export const fetchPosts = (boardSlug, params = {}) =>
|
||||
boardApi.get(`/api/boards/${boardSlug}/posts/`, { params });
|
||||
|
||||
// ✅ 특정 게시글 상세 조회
|
||||
export const fetchPostDetail = (boardSlug, postId) =>
|
||||
boardApi.get(`/api/boards/${boardSlug}/posts/${postId}/`);
|
||||
|
||||
// ✅ 게시글 생성
|
||||
export const createPost = (boardSlug, data) =>
|
||||
boardApi.post(`/api/boards/${boardSlug}/posts/`, data);
|
||||
|
||||
// ✅ 게시글 수정
|
||||
export const updatePost = (boardSlug, postId, data) =>
|
||||
boardApi.put(`/api/boards/${boardSlug}/posts/${postId}/`, data);
|
||||
|
||||
// ✅ 게시글 삭제
|
||||
export const deletePost = (boardSlug, postId) =>
|
||||
boardApi.delete(`/api/boards/${boardSlug}/posts/${postId}/`);
|
||||
|
||||
export default boardApi;
|
40
src/components/Board/BoardList.js
Normal file
40
src/components/Board/BoardList.js
Normal file
@ -0,0 +1,40 @@
|
||||
// src/components/Board/BoardList.js
|
||||
import React, { useEffect, useState } from "react";
|
||||
import boardApi from "../../api/boardApi";
|
||||
|
||||
const BoardList = ({ onSelectBoard, selectedBoard }) => {
|
||||
const [boards, setBoards] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBoards = async () => {
|
||||
try {
|
||||
const res = await boardApi.get("/api/boards/");
|
||||
setBoards(res.data);
|
||||
} catch (err) {
|
||||
console.error("❌ 게시판 목록 불러오기 실패", err);
|
||||
}
|
||||
};
|
||||
fetchBoards();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">📁 게시판</h2>
|
||||
<ul className="space-y-1">
|
||||
{boards.map((board) => (
|
||||
<li
|
||||
key={board.slug}
|
||||
onClick={() => onSelectBoard(board.slug)}
|
||||
className={`cursor-pointer px-2 py-1 rounded hover:bg-blue-100 ${
|
||||
selectedBoard === board.slug ? "bg-blue-200 font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{board.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardList;
|
45
src/components/Board/PostList.js
Normal file
45
src/components/Board/PostList.js
Normal file
@ -0,0 +1,45 @@
|
||||
// src/components/Board/PostList.js
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { fetchPosts } from "../../api/boardApi";
|
||||
|
||||
const PostList = ({ boardSlug, search, tag }) => {
|
||||
const [posts, setPosts] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!boardSlug) return;
|
||||
|
||||
const loadPosts = async () => {
|
||||
try {
|
||||
const params = {};
|
||||
if (tag) params.tag = tag;
|
||||
if (search) params.search = search;
|
||||
|
||||
const res = await fetchPosts(boardSlug, params);
|
||||
setPosts(res.data);
|
||||
} catch (err) {
|
||||
console.error("❌ 게시글 목록 불러오기 실패", err);
|
||||
}
|
||||
};
|
||||
|
||||
loadPosts();
|
||||
}, [boardSlug, search, tag]);
|
||||
|
||||
if (!boardSlug) return <p>📂 게시판을 선택해주세요.</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">📝 게시글 목록</h2>
|
||||
<ul className="space-y-2">
|
||||
{posts.map((post) => (
|
||||
<li key={post.id} className="border p-2 rounded shadow">
|
||||
<h3 className="font-semibold">{post.title}</h3>
|
||||
<p className="text-sm text-gray-500">작성자: {post.author_name}</p>
|
||||
<p className="text-gray-700 line-clamp-2">{post.content}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostList;
|
72
src/components/Board/PostSearchPanel.js
Normal file
72
src/components/Board/PostSearchPanel.js
Normal file
@ -0,0 +1,72 @@
|
||||
// src/components/Board/PostSearchPanel.js
|
||||
import React, { useEffect, useState } from "react";
|
||||
import boardApi from "../../api/boardApi";
|
||||
|
||||
const PostSearchPanel = ({ boardSlug, onSearch, onTagSelect }) => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [tags, setTags] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!boardSlug) {
|
||||
setTags([]); // ✅ 게시판 선택 해제 시 태그도 비움
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const res = await boardApi.get(`/api/boards/${boardSlug}/tags/`);
|
||||
setTags(res.data); // ['python', 'devops', ...]
|
||||
} catch (err) {
|
||||
console.error("❌ 태그 불러오기 실패", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTags();
|
||||
}, [boardSlug]);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
onSearch(searchInput);
|
||||
};
|
||||
|
||||
// ✅ boardSlug 없으면 UI 자체를 숨김
|
||||
if (!boardSlug) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">🔍 검색 및 태그</h2>
|
||||
<form onSubmit={handleSearch} className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="검색어 입력"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="w-full border px-2 py-1 rounded mb-2"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 text-white py-1 rounded"
|
||||
>
|
||||
검색
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">📌 태그 목록</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
onClick={() => onTagSelect(tag)}
|
||||
className="cursor-pointer bg-gray-200 px-2 py-1 rounded hover:bg-blue-200"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostSearchPanel;
|
37
src/components/Board/_unused_BoardNavLinks.js
Normal file
37
src/components/Board/_unused_BoardNavLinks.js
Normal file
@ -0,0 +1,37 @@
|
||||
// src/components/Board/BoardNavLinks.js
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import boardApi from "../../api/boardApi";
|
||||
|
||||
const BoardNavLinks = () => {
|
||||
const [boards, setBoards] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBoards = async () => {
|
||||
try {
|
||||
const res = await boardApi.get("/api/boards/");
|
||||
setBoards(res.data);
|
||||
} catch (err) {
|
||||
console.error("❌ 게시판 목록 불러오기 실패", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBoards();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{boards.map((board) => (
|
||||
<Link
|
||||
key={board.slug}
|
||||
to={`/api/boards/${board.slug}`}
|
||||
className="text-gray-600 hover:text-[#D4AF37]"
|
||||
>
|
||||
{board.name}
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardNavLinks;
|
@ -6,16 +6,19 @@ const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isLoggedIn, logout, user } = useAuth();
|
||||
|
||||
// ✅ 로그인 상태에 따라 메뉴 구성
|
||||
// 로그인 상태에 따라 메뉴 구성
|
||||
const menuItems = [
|
||||
{ name: "home", path: "/" },
|
||||
{ name: "about", path: "/about" },
|
||||
{ name: "posts", path: "/posts" },
|
||||
{ name: "boards", path: "/boards" },
|
||||
];
|
||||
|
||||
// 로그인 상태일 때 추가되는 메뉴
|
||||
if (isLoggedIn) {
|
||||
menuItems.push({ name: "tasks", path: "/tasks" });
|
||||
menuItems.push({ name: "ansible", path: "/ansible" });
|
||||
// menuItems.push({ name: "boards", path: "/boards" });
|
||||
}
|
||||
|
||||
return (
|
||||
|
81
src/components/_unused_Navbar.js
Normal file
81
src/components/_unused_Navbar.js
Normal file
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
|
||||
const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isLoggedIn, logout, user } = useAuth();
|
||||
|
||||
// ✅ 로그인 상태에 따라 메뉴 구성
|
||||
const menuItems = [
|
||||
{ name: "home", path: "/" },
|
||||
{ name: "about", path: "/about" },
|
||||
{ name: "posts", path: "/posts" },
|
||||
];
|
||||
|
||||
if (isLoggedIn) {
|
||||
menuItems.push({ name: "tasks", path: "/tasks" });
|
||||
menuItems.push({ name: "ansible", path: "/ansible" });
|
||||
}
|
||||
|
||||
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]">
|
||||
Dev
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{menuItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className="text-gray-600 hover:text-[#D4AF37]"
|
||||
>
|
||||
{item.name.charAt(0).toUpperCase() + item.name.slice(1)}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
내 정보 {user?.grade && `(${user.grade})`}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
}}
|
||||
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||||
>
|
||||
로그아웃
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
43
src/pages/BoardsPage.js
Normal file
43
src/pages/BoardsPage.js
Normal file
@ -0,0 +1,43 @@
|
||||
// src/pages/BoardsPage.js
|
||||
import React, { useState } from "react";
|
||||
import BoardList from "../components/Board/BoardList";
|
||||
import PostList from "../components/Board/PostList";
|
||||
import PostSearchPanel from "../components/Board/PostSearchPanel";
|
||||
|
||||
const BoardsPage = () => {
|
||||
const [selectedBoard, setSelectedBoard] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedTag, setSelectedTag] = useState("");
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 p-4">
|
||||
{/* 좌측: 게시판 리스트 */}
|
||||
<div className="col-span-2 bg-white p-4 shadow rounded">
|
||||
<BoardList
|
||||
onSelectBoard={setSelectedBoard}
|
||||
selectedBoard={selectedBoard}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 중앙: 게시글 리스트 */}
|
||||
<div className="col-span-7 bg-white p-4 shadow rounded">
|
||||
<PostList
|
||||
boardSlug={selectedBoard}
|
||||
search={searchQuery}
|
||||
tag={selectedTag}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 우측: 검색 + 태그 */}
|
||||
<div className="col-span-3 bg-white p-4 shadow rounded">
|
||||
<PostSearchPanel
|
||||
boardSlug={selectedBoard}
|
||||
onSearch={setSearchQuery}
|
||||
onTagSelect={setSelectedTag}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardsPage;
|
Reference in New Issue
Block a user