diff --git a/.env.development b/.env.development
index 50a8e7a..cedec6c 100644
--- a/.env.development
+++ b/.env.development
@@ -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
\ No newline at end of file
diff --git a/.env.production b/.env.production
index 0ed72c9..db20d2a 100644
--- a/.env.production
+++ b/.env.production
@@ -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
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index c498fe3..e9be491 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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() {
} />
} />
} />
+ } />
} />
diff --git a/src/api/boardApi.js b/src/api/boardApi.js
new file mode 100644
index 0000000..ea4ad90
--- /dev/null
+++ b/src/api/boardApi.js
@@ -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;
diff --git a/src/components/Board/BoardList.js b/src/components/Board/BoardList.js
new file mode 100644
index 0000000..65d6d7c
--- /dev/null
+++ b/src/components/Board/BoardList.js
@@ -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 (
+
+
📁 게시판
+
+ {boards.map((board) => (
+ 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}
+
+ ))}
+
+
+ );
+};
+
+export default BoardList;
\ No newline at end of file
diff --git a/src/components/Board/PostList.js b/src/components/Board/PostList.js
new file mode 100644
index 0000000..e6a9f26
--- /dev/null
+++ b/src/components/Board/PostList.js
@@ -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 📂 게시판을 선택해주세요.
;
+
+ return (
+
+
📝 게시글 목록
+
+ {posts.map((post) => (
+
+ {post.title}
+ 작성자: {post.author_name}
+ {post.content}
+
+ ))}
+
+
+ );
+};
+
+export default PostList;
diff --git a/src/components/Board/PostSearchPanel.js b/src/components/Board/PostSearchPanel.js
new file mode 100644
index 0000000..5076233
--- /dev/null
+++ b/src/components/Board/PostSearchPanel.js
@@ -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 (
+
+
🔍 검색 및 태그
+
+
+
+
📌 태그 목록
+
+ {tags.map((tag) => (
+ onTagSelect(tag)}
+ className="cursor-pointer bg-gray-200 px-2 py-1 rounded hover:bg-blue-200"
+ >
+ #{tag}
+
+ ))}
+
+
+
+ );
+};
+
+export default PostSearchPanel;
diff --git a/src/components/Board/_unused_BoardNavLinks.js b/src/components/Board/_unused_BoardNavLinks.js
new file mode 100644
index 0000000..3b8e977
--- /dev/null
+++ b/src/components/Board/_unused_BoardNavLinks.js
@@ -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) => (
+
+ {board.name}
+
+ ))}
+ >
+ );
+};
+
+export default BoardNavLinks;
diff --git a/src/components/Navbar.js b/src/components/Navbar.js
index 3229d42..876d2a8 100644
--- a/src/components/Navbar.js
+++ b/src/components/Navbar.js
@@ -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 (
diff --git a/src/components/_unused_Navbar.js b/src/components/_unused_Navbar.js
new file mode 100644
index 0000000..3229d42
--- /dev/null
+++ b/src/components/_unused_Navbar.js
@@ -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 (
+
+
+
+
+ Dev
+
+
+
+ {menuItems.map((item) => (
+
+ {item.name.charAt(0).toUpperCase() + item.name.slice(1)}
+
+ ))}
+
+ {isLoggedIn ? (
+ <>
+
+ 내 정보 {user?.grade && `(${user.grade})`}
+
+ {
+ logout();
+ navigate("/login");
+ }}
+ className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
+ >
+ 로그아웃
+
+ >
+ ) : (
+ <>
+
+ 로그인
+
+
+ 회원가입
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default Navbar;
diff --git a/src/pages/BoardsPage.js b/src/pages/BoardsPage.js
new file mode 100644
index 0000000..5ff64d6
--- /dev/null
+++ b/src/pages/BoardsPage.js
@@ -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 (
+
+ {/* 좌측: 게시판 리스트 */}
+
+
+
+
+ {/* 중앙: 게시글 리스트 */}
+
+
+ {/* 우측: 검색 + 태그 */}
+
+
+ );
+};
+
+export default BoardsPage;
\ No newline at end of file
diff --git a/version b/version
index 1111c9c..9beca35 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.0.14
\ No newline at end of file
+0.0.15
\ No newline at end of file