게시판 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m33s

This commit is contained in:
2025-05-22 23:51:08 +09:00
parent c262b1c863
commit 8997539c9a
12 changed files with 365 additions and 2 deletions

View File

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

View File

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

View File

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

View 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;

View 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;

View 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;

View 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;

View File

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

View 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
View 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;

View File

@ -1 +1 @@
0.0.14
0.0.15