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

🔍 검색 및 태그

+
+ setSearchInput(e.target.value)} + className="w-full border px-2 py-1 rounded mb-2" + /> + +
+ +
+

📌 태그 목록

+
+ {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 ( + + ); +}; + +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