This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
REACT_APP_API_AUTH=http://127.0.0.1:8000
|
REACT_APP_API_AUTH=http://127.0.0.1:8000
|
||||||
REACT_APP_API_BLOG=http://127.0.0.1:8800
|
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_TODO=http://127.0.0.1:8880
|
||||||
REACT_APP_API_ANSIBLE=http://127.0.0.1:8888
|
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_AUTH=https://www.icurfer.com
|
||||||
REACT_APP_API_BLOG=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_TODO=https://www.icurfer.com
|
||||||
REACT_APP_API_ANSIBLE=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 Register from './pages/Register';
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
import TaskPage from "./pages/TaskPage";
|
import TaskPage from "./pages/TaskPage";
|
||||||
|
import BoardsPage from "./pages/BoardsPage";
|
||||||
import AnsiblePage from "./pages/Ansible";
|
import AnsiblePage from "./pages/Ansible";
|
||||||
|
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
@ -36,6 +37,7 @@ function App() {
|
|||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/tasks" element={<TaskPage />} />
|
<Route path="/tasks" element={<TaskPage />} />
|
||||||
|
<Route path="/boards" element={<BoardsPage />} />
|
||||||
<Route path="/ansible" element={<AnsiblePage />} />
|
<Route path="/ansible" element={<AnsiblePage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</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 navigate = useNavigate();
|
||||||
const { isLoggedIn, logout, user } = useAuth();
|
const { isLoggedIn, logout, user } = useAuth();
|
||||||
|
|
||||||
// ✅ 로그인 상태에 따라 메뉴 구성
|
// 로그인 상태에 따라 메뉴 구성
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ name: "home", path: "/" },
|
{ name: "home", path: "/" },
|
||||||
{ name: "about", path: "/about" },
|
{ name: "about", path: "/about" },
|
||||||
{ name: "posts", path: "/posts" },
|
{ name: "posts", path: "/posts" },
|
||||||
|
{ name: "boards", path: "/boards" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 로그인 상태일 때 추가되는 메뉴
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
menuItems.push({ name: "tasks", path: "/tasks" });
|
menuItems.push({ name: "tasks", path: "/tasks" });
|
||||||
menuItems.push({ name: "ansible", path: "/ansible" });
|
menuItems.push({ name: "ansible", path: "/ansible" });
|
||||||
|
// menuItems.push({ name: "boards", path: "/boards" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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