Krafton Jungle/6. Frameworks

[WEEK14] Day 5

munsik22 2025. 6. 16. 11:26

게시판 구현하기

프로젝트 주제도 정했으니 이제 본격적으로 게시판을 구현해보자.

  • MySQL에서 다음 쿼리문을 입력해 테이블을 생성한다.
    • tags: 태그 테이블 (예: #React, #Spring, #SQL 등)
    • posts: 게시글 테이블
    • post_tags: 게시글-태그 매핑 테이블 (게시글과 태그를 연결하는 중간 테이블)
CREATE TABLE tags (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE posts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  author_id INT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE post_tags (
  post_id INT NOT NULL,
  tag_id INT NOT NULL,
  PRIMARY KEY (post_id, tag_id),
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);

게시글 조회하기

  • 게시글 조회용으로 /routes/posts.js 파일을 작성한다.
const express = require("express");
const router = express.Router();
const db = require("../db");

router.get("/", async (req, res) => {
    try {
        const [rows] = await db.query(`
            SELECT 
                posts.id,
                posts.title,
                userinfo.name AS author,
                posts.created_at
            FROM posts
            JOIN userinfo ON posts.author_id = userinfo.id
            ORDER BY posts.id DESC
        `);
        res.json(rows);
    } catch (err) {
        console.error("DB 오류:", err);
        res.status(500).json({ error: "서버 오류" });
    }
});

module.exports = router;
  • index.js 파일을 수정한다.
const postsRouter = require("./routes/posts");
app.use("/api/posts", postsRouter);
  • Home.jsx를 수정한다.
const [posts, setPosts] = useState([]);

useEffect(() => {
        fetch("/api/posts")
            .then((res) => res.json())
            .then((data) => setPosts(data));
}, []);

return (
        <div className="board-wrapper">
            <h1 className="board-title">TIL Board</h1>
            <table className="board-table">
                <thead>
                    <tr>
                        <th>번호</th>
                        <th>제목</th>
                        <th>작성자</th>
                        <th>작성일</th>
                    </tr>
                </thead>
                <tbody>
                    {posts.map((post) => (
                        <tr key={post.id}>
                            <td>{post.id}</td>
                            <td>{post.title}</td>
                            <td>{post.author}</td>
                            <td>
                                {new Date(post.created_at).toLocaleString()}
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );

구현 예시

페이지네이션 적용

페이지네이션은 많은 양의 콘텐츠를 탐색하기 쉽도록 여러 화면에 나누고, 분할된 화면을 탐색하는 데 사용되는 요소이다. #
  • /routes/posts.js를 수정한다.
const express = require("express");
const router = express.Router();
const db = require("../db");

router.get("/", async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const pageSize = 10;
    const offset = (page - 1) * pageSize;

    try {
        const [rows] = await db.query(`
            SELECT 
                posts.id,
                posts.title,
                userinfo.username AS author,
                posts.created_at
            FROM posts
            JOIN userinfo ON posts.author_id = userinfo.id
            ORDER BY posts.id DESC
            LIMIT ? OFFSET ?
        `, [pageSize, offset]);

        const [countResult] = await db.query(
            `SELECT COUNT(*) AS total FROM posts`
        );
        const total = countResult[0].total;

        res.json({
            posts: rows,
            total,
            page,
            totalPages: Math.ceil(total / pageSize),
        });
    } catch (err) {
        console.error("DB 오류:", err);
        res.status(500).json({ error: "서버 오류" });
    }
});

module.exports = router;
  • Home.jsx를 수정한다.
function Home() {
    const [posts, setPosts] = useState([]);
    const [totalPages, setTotalPages] = useState(1);
    const [searchParams, setSearchParams] = useSearchParams();
    const page = parseInt(searchParams.get("page")) || 1;

    const fetchPosts = async (pageNum) => {
        const res = await fetch(`/api/posts?page=${pageNum}`);
        const data = await res.json();
        setPosts(data.posts);
        setTotalPages(data.totalPages);
        setSearchParams({ page: pageNum });
    };

    const renderPagination = () => {
        const buttons = [];
        const maxPage = totalPages;
        const current = page;
        // 이전 버튼
        buttons.push(
            <button
            key="prev"
            onClick={() => fetchPosts(current - 1)}
            disabled={current === 1}
            className={current === 1 ? "disabled" : ""}
            >
            ◀︎ 이전
            </button>
        );
        // 1번 페이지
        buttons.push(
            <button key={1} onClick={() => fetchPosts(1)} className={current === 1 ? "active" : ""}>
                1
            </button>
        );
        // … 왼쪽
        if (current > 3) {
            buttons.push(<span key="start-ellipsis">…</span>);
        }
        // 현재 기준 ±1 페이지
        for (let i = current - 1; i <= current + 1; i++) {
            if (i > 1 && i < maxPage) {
                buttons.push(
                    <button
                        key={i}
                        onClick={() => fetchPosts(i)}
                        className={current === i ? "active" : ""}
                    >
                        {i}
                    </button>
                );
            }
        }
        // … 오른쪽
        if (current < maxPage - 2) {
            buttons.push(<span key="end-ellipsis">…</span>);
        }
        // 마지막 페이지
        if (maxPage > 1) {
            buttons.push(
                <button key={maxPage} onClick={() => fetchPosts(maxPage)} className={current === maxPage ? "active" : ""}>
                    {maxPage}
                </button>
            );
        }
        // 다음 버튼
        buttons.push(
            <button
            key="next"
            onClick={() => fetchPosts(current + 1)}
            disabled={current === maxPage}
            className={current === maxPage ? "disabled" : ""}
            >
            다음 ▶︎
            </button>
        );
        return <div className="pagination">{buttons}</div>;
    };

    useEffect(() => {
        fetchPosts(page);
    }, [page]);

    return (
        <div className="board-wrapper">
            <h1 className="board-title">TIL Board</h1>
            <table className="board-table">
                <thead>
                    <tr>
                        <th>번호</th>
                        <th>제목</th>
                        <th>작성자</th>
                        <th>작성일</th>
                    </tr>
                </thead>
                <tbody>
                    {posts.map((post) => (
                        <tr key={post.id}>
                            <td>{post.id}</td>
                            <td>{post.title}</td>
                            <td>{post.author}</td>
                            <td>
                                {new Date(post.created_at).toLocaleString()}
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
            <div className="pagination-wrapper">
                {renderPagination()}
            </div>
        </div>
    );
}

구현 예시

 

'Krafton Jungle > 6. Frameworks' 카테고리의 다른 글

[WEEK14] Day 6  (0) 2025.06.17
[WEEK14] Day 3  (0) 2025.06.14
[WEEK14] Day 2  (0) 2025.06.13
[WEEK14] Day 1  (0) 2025.06.12