게시물 상세보기, 삭제 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m32s
All checks were successful
Build And Test / build-and-push (push) Successful in 1m32s
This commit is contained in:
@ -1,38 +1,51 @@
|
|||||||
// src/api/boardApi.js
|
import axios from "axios";
|
||||||
import axios from 'axios';
|
import { attachAuthInterceptors } from "./attachInterceptors";
|
||||||
import { attachAuthInterceptors } from './attachInterceptors';
|
|
||||||
|
|
||||||
const boardApi = axios.create({
|
const boardApi = axios.create({
|
||||||
baseURL: process.env.REACT_APP_API_BOARD, // ex) http://localhost:8801/api
|
baseURL: process.env.REACT_APP_API_BOARD, // ex) http://localhost:8801/api
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// JWT 인증 토큰 인터셉터 부착
|
// ✅ JWT 토큰 자동 포함
|
||||||
attachAuthInterceptors(boardApi);
|
attachAuthInterceptors(boardApi);
|
||||||
|
|
||||||
// ✅ 게시판 목록 조회
|
//
|
||||||
export const fetchBoards = () => boardApi.get('/api/boards/');
|
// ✅ 게시판 관련
|
||||||
|
//
|
||||||
|
|
||||||
// ✅ 특정 게시판의 게시글 목록 조회
|
// 게시판 목록 조회
|
||||||
|
export const fetchBoards = () => boardApi.get("/api/boards/");
|
||||||
|
|
||||||
|
//
|
||||||
|
// ✅ 게시글 관련 (게시판 내 nested 구조)
|
||||||
|
//
|
||||||
|
|
||||||
|
// 특정 게시판의 게시글 목록 조회
|
||||||
export const fetchPosts = (boardSlug, params = {}) =>
|
export const fetchPosts = (boardSlug, params = {}) =>
|
||||||
boardApi.get(`/api/boards/${boardSlug}/posts/`, { params });
|
boardApi.get(`/api/boards/${boardSlug}/posts/`, { params });
|
||||||
|
|
||||||
// ✅ 특정 게시글 상세 조회
|
|
||||||
export const fetchPostDetail = (boardSlug, postId) =>
|
export const fetchPostDetail = (boardSlug, postId) =>
|
||||||
boardApi.get(`/api/boards/${boardSlug}/posts/${postId}/`);
|
boardApi.get(`/api/boards/${boardSlug}/posts/${postId}/`);
|
||||||
|
|
||||||
// ✅ 게시글 생성
|
// 게시글 생성
|
||||||
export const createPost = (boardSlug, data) =>
|
export const createPost = (boardSlug, data) =>
|
||||||
boardApi.post(`/api/boards/${boardSlug}/posts/`, data);
|
boardApi.post(`/api/boards/${boardSlug}/posts/`, data);
|
||||||
|
|
||||||
// ✅ 게시글 수정
|
// 게시글 수정
|
||||||
export const updatePost = (boardSlug, postId, data) =>
|
export const updatePost = (boardSlug, postId, data) =>
|
||||||
boardApi.put(`/api/boards/${boardSlug}/posts/${postId}/`, data);
|
boardApi.put(`/api/boards/${boardSlug}/posts/${postId}/`, data);
|
||||||
|
|
||||||
// ✅ 게시글 삭제
|
// 게시글 삭제
|
||||||
export const deletePost = (boardSlug, postId) =>
|
export const deletePost = (boardSlug, postId) =>
|
||||||
boardApi.delete(`/api/boards/${boardSlug}/posts/${postId}/`);
|
boardApi.delete(`/api/boards/${boardSlug}/posts/${postId}/`);
|
||||||
|
|
||||||
|
//
|
||||||
|
// ✅ 게시글 단일 조회 (boardSlug 없이 ID만으로 조회)
|
||||||
|
//
|
||||||
|
|
||||||
|
// 단일 게시글 상세 조회 (id 기반)
|
||||||
|
export const getPost = (postId) => boardApi.get(`/api/posts/${postId}/`);
|
||||||
|
|
||||||
export default boardApi;
|
export default boardApi;
|
||||||
|
71
src/components/Board/PostDetail.js
Normal file
71
src/components/Board/PostDetail.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// src/components/Board/PostDetail.js
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { fetchPostDetail, deletePost } from "../../api/boardApi";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
|
||||||
|
const PostDetail = ({ postId, boardSlug, onClose, onDeleted }) => {
|
||||||
|
const [post, setPost] = useState(null);
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetch = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetchPostDetail(boardSlug, postId); // ✅ boardSlug 포함 요청
|
||||||
|
setPost(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ 게시물 조회 실패", err);
|
||||||
|
alert("게시글을 불러오지 못했습니다.");
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetch();
|
||||||
|
}, [postId, boardSlug, onClose]); // ✅ boardSlug 의존성 포함
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!window.confirm("정말 삭제하시겠습니까?")) return;
|
||||||
|
try {
|
||||||
|
await deletePost(boardSlug, postId); // ✅ boardSlug 포함
|
||||||
|
alert("✅ 삭제되었습니다.");
|
||||||
|
onDeleted();
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ 삭제 실패", err);
|
||||||
|
alert("삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!post) return null;
|
||||||
|
|
||||||
|
const isAuthor = user?.email === post.author_name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded shadow bg-white">
|
||||||
|
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
|
||||||
|
<p className="text-gray-600 mb-1">작성자: {post.author_name}</p>
|
||||||
|
<p className="text-gray-500 text-sm mb-4">
|
||||||
|
작성일: {new Date(post.created_at).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="whitespace-pre-wrap border-t pt-4">{post.content}</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-1 bg-gray-300 text-black rounded"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
{isAuthor && (
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="px-4 py-1 bg-red-500 text-white rounded"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostDetail;
|
@ -1,4 +1,3 @@
|
|||||||
// src/components/Board/PostForm.js
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { createPost } from "../../api/boardApi";
|
import { createPost } from "../../api/boardApi";
|
||||||
|
|
||||||
@ -34,11 +33,11 @@ const PostForm = ({ boardSlug, onClose, onCreated }) => {
|
|||||||
console.log("✅ 전송할 payload:", payload);
|
console.log("✅ 전송할 payload:", payload);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createPost(boardSlug, payload);
|
await createPost(boardSlug, payload); // ✅ 백엔드 호환: slug 사용
|
||||||
alert("✅ 게시글이 등록되었습니다.");
|
alert("✅ 게시글이 등록되었습니다.");
|
||||||
setForm({ title: "", content: "", tags: "" });
|
setForm({ title: "", content: "", tags: "" }); // 폼 초기화
|
||||||
onCreated();
|
onCreated(); // 목록 새로고침 트리거
|
||||||
onClose();
|
onClose(); // 폼 닫기
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ 게시글 등록 실패", err);
|
console.error("❌ 게시글 등록 실패", err);
|
||||||
console.log("🚨 서버 응답 내용:", err.response?.data);
|
console.log("🚨 서버 응답 내용:", err.response?.data);
|
||||||
|
@ -1,43 +1,113 @@
|
|||||||
// src/components/Board/PostList.js
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { fetchPosts } from "../../api/boardApi";
|
import { fetchPosts } from "../../api/boardApi";
|
||||||
|
import PostDetail from "./PostDetail";
|
||||||
|
|
||||||
const PostList = ({ boardSlug, search, tag }) => {
|
const PostList = ({ boardSlug, search, tag }) => {
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
|
const [selectedPostId, setSelectedPostId] = useState(null);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const loadPosts = useCallback(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);
|
||||||
|
}
|
||||||
|
}, [boardSlug, tag, search]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!boardSlug) return;
|
if (boardSlug) {
|
||||||
|
loadPosts();
|
||||||
|
}
|
||||||
|
}, [boardSlug, loadPosts]);
|
||||||
|
|
||||||
const loadPosts = async () => {
|
// ✅ ESC 키로 닫기
|
||||||
try {
|
useEffect(() => {
|
||||||
const params = {};
|
const handleKeyDown = (e) => {
|
||||||
if (tag) params.tag = tag;
|
if (e.key === "Escape") {
|
||||||
if (search) params.search = search;
|
setDrawerOpen(false);
|
||||||
|
setSelectedPostId(null);
|
||||||
const res = await fetchPosts(boardSlug, params);
|
|
||||||
setPosts(res.data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ 게시글 목록 불러오기 실패", err);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPosts();
|
if (drawerOpen) {
|
||||||
}, [boardSlug, search, tag]);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [drawerOpen]);
|
||||||
|
|
||||||
|
const handleSelectPost = (postId) => {
|
||||||
|
setSelectedPostId(postId);
|
||||||
|
setDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDrawer = () => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
setSelectedPostId(null);
|
||||||
|
};
|
||||||
|
|
||||||
if (!boardSlug) return <p>📂 게시판을 선택해주세요.</p>;
|
if (!boardSlug) return <p>📂 게시판을 선택해주세요.</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative">
|
||||||
<h2 className="text-lg font-bold mb-2">📝 게시글 목록</h2>
|
<h2 className="text-lg font-bold mb-2">📝 게시글 목록</h2>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<li key={post.id} className="border p-2 rounded shadow">
|
<li
|
||||||
|
key={post.id}
|
||||||
|
className="border p-2 rounded shadow cursor-pointer hover:bg-gray-100"
|
||||||
|
onClick={() => handleSelectPost(post.id)}
|
||||||
|
>
|
||||||
<h3 className="font-semibold">{post.title}</h3>
|
<h3 className="font-semibold">{post.title}</h3>
|
||||||
<p className="text-sm text-gray-500">작성자: {post.author_name}</p>
|
<p className="text-sm text-gray-500">작성자: {post.author_name}</p>
|
||||||
<p className="text-gray-700 line-clamp-2">{post.content}</p>
|
<p className="text-gray-700 line-clamp-2">{post.content}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{/* ✅ 슬라이딩 Drawer */}
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 right-0 h-full w-full md:w-[480px] bg-white shadow-lg z-50 transform transition-transform duration-300 ${
|
||||||
|
drawerOpen ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-end p-4 border-b">
|
||||||
|
<button
|
||||||
|
onClick={handleCloseDrawer}
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
>
|
||||||
|
✖ 닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 overflow-y-auto h-[calc(100%-64px)]">
|
||||||
|
{selectedPostId && (
|
||||||
|
<PostDetail
|
||||||
|
postId={selectedPostId}
|
||||||
|
boardSlug={boardSlug} // ✅ 필수
|
||||||
|
onClose={handleCloseDrawer}
|
||||||
|
onDeleted={loadPosts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ 배경 오버레이 */}
|
||||||
|
{drawerOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-30 z-40"
|
||||||
|
onClick={handleCloseDrawer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user