게시판 만들기
게시판이란?
게시판은 웹 개발의 대표적인 CRUD 예제입니다.
CRUD는 Create(글쓰기), Read(읽기), Update(수정), Delete(삭제) 의 앞 글자를 딴 말입니다.
이 네 가지 동작을 구현할 수 있으면, 웹 서비스에서 데이터를 다루는 기본기가 갖춰진 겁니다.
회원 관리, 상품 관리, 댓글… 모두 CRUD의 응용입니다.
데이터베이스 설계
CREATE TABLE board (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
email VARCHAR(100) NULL,
pass VARCHAR(255) NOT NULL,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
wdate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip VARCHAR(45) NOT NULL,
view INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ip VARCHAR(45)는 IPv6 주소까지 저장할 수 있는 길이입니다.
DB 연결 (db.php)
모든 파일에서 공통으로 require해서 씁니다.
<?php
declare(strict_types=1);
$dsn = 'mysql:host=localhost;dbname=mydb;charset=utf8mb4';
try {
$pdo = new PDO($dsn, '아이디', '비밀번호', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
error_log($e->getMessage());
exit('데이터베이스 연결에 실패했습니다.');
}
글 목록 (list.php)
<?php
declare(strict_types=1);
require 'db.php';
$pageSize = 10;
$pageListSize = 10;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $pageSize;
$total = (int) $pdo->query("SELECT COUNT(*) FROM board")->fetchColumn();
$totalPage = (int) ceil($total / $pageSize);
$stmt = $pdo->prepare(
"SELECT * FROM board ORDER BY id DESC LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(':limit', $pageSize, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>게시판</title></head>
<body>
<table border="1" width="580">
<tr>
<th width="40">번호</th>
<th>제목</th>
<th width="80">글쓴이</th>
<th width="80">날짜</th>
<th width="50">조회</th>
</tr>
<?php foreach ($rows as $row): ?>
<tr>
<td align="center"><?= $row['id'] ?></td>
<td>
<a href="read.php?id=<?= $row['id'] ?>&page=<?= $page ?>">
<?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?>
</a>
</td>
<td align="center"><?= htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td align="center"><?= substr($row['wdate'], 0, 10) ?></td>
<td align="center"><?= $row['view'] ?></td>
</tr>
<?php endforeach; ?>
</table>
<!-- 페이지 네비게이션 -->
<div>
<?php
$startPage = (int) (floor(($page - 1) / $pageListSize) * $pageListSize + 1);
$endPage = min($startPage + $pageListSize - 1, $totalPage);
if ($startPage > 1) {
echo "<a href='?page=" . ($startPage - 1) . "'>◀</a> ";
}
for ($i = $startPage; $i <= $endPage; $i++) {
if ($i === $page) {
echo " <strong>{$i}</strong> ";
} else {
echo " <a href='?page={$i}'>{$i}</a> ";
}
}
if ($endPage < $totalPage) {
echo " <a href='?page=" . ($endPage + 1) . "'>▶</a>";
}
?>
</div>
<a href="write.php">글쓰기</a>
</body>
</html>
글쓰기 폼 (write.php)
<?php declare(strict_types=1); ?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>글쓰기</title></head>
<body>
<form action="insert.php" method="post">
<table border="1" width="580">
<tr><td colspan="2" align="center"><b>글 쓰 기</b></td></tr>
<tr>
<td width="80">이름</td>
<td><input type="text" name="name" maxlength="20" required></td>
</tr>
<tr>
<td>이메일</td>
<td><input type="text" name="email" maxlength="100"></td>
</tr>
<tr>
<td>비밀번호</td>
<td>
<input type="password" name="pass" maxlength="20" required>
<small>(수정·삭제 시 필요)</small>
</td>
</tr>
<tr>
<td>제목</td>
<td><input type="text" name="title" size="60" maxlength="100" required></td>
</tr>
<tr>
<td>내용</td>
<td><textarea name="content" cols="65" rows="15" required></textarea></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="저장">
<input type="button" value="취소" onclick="history.back()">
</td>
</tr>
</table>
</form>
</body>
</html>
글 저장 (insert.php)
<?php
declare(strict_types=1);
require 'db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: list.php');
exit;
}
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$pass = trim($_POST['pass'] ?? '');
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
if ($name === '' || $pass === '' || $title === '' || $content === '') {
exit('필수 항목을 모두 입력해주세요.');
}
$stmt = $pdo->prepare(
"INSERT INTO board (name, email, pass, title, content, ip)
VALUES (:name, :email, :pass, :title, :content, :ip)"
);
$stmt->execute([
':name' => $name,
':email' => $email,
':pass' => password_hash($pass, PASSWORD_DEFAULT),
':title' => $title,
':content' => $content,
':ip' => $_SERVER['REMOTE_ADDR'],
]);
header('Location: list.php');
exit;
글 읽기 (read.php)
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_GET['id'] ?? 0);
$page = (int) ($_GET['page'] ?? 1);
if ($id === 0) {
header('Location: list.php');
exit;
}
// 조회수 증가
$pdo->prepare("UPDATE board SET view = view + 1 WHERE id = :id")
->execute([':id' => $id]);
$stmt = $pdo->prepare("SELECT * FROM board WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row) {
exit('존재하지 않는 글입니다.');
}
// 이전/다음 글
$prev = $pdo->prepare("SELECT id, title FROM board WHERE id > :id ORDER BY id ASC LIMIT 1");
$next = $pdo->prepare("SELECT id, title FROM board WHERE id < :id ORDER BY id DESC LIMIT 1");
$prev->execute([':id' => $id]);
$next->execute([':id' => $id]);
$prevRow = $prev->fetch();
$nextRow = $next->fetch();
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title><?= h($row['title']) ?></title></head>
<body>
<table border="1" width="580">
<tr>
<th colspan="4"><?= h($row['title']) ?></th>
</tr>
<tr>
<td>글쓴이</td><td><?= h($row['name']) ?></td>
<td>날짜</td><td><?= $row['wdate'] ?></td>
</tr>
<tr>
<td>이메일</td><td><?= h($row['email']) ?></td>
<td>조회수</td><td><?= $row['view'] ?></td>
</tr>
<tr>
<td colspan="4"><?= nl2br(h($row['content'])) ?></td>
</tr>
<tr>
<td colspan="4">
<a href="list.php?page=<?= $page ?>">[목록]</a>
<a href="write.php">[글쓰기]</a>
<a href="edit.php?id=<?= $id ?>&page=<?= $page ?>">[수정]</a>
<a href="delete.php?id=<?= $id ?>&page=<?= $page ?>">[삭제]</a>
<?php if ($prevRow): ?>
<a href="read.php?id=<?= $prevRow['id'] ?>&page=<?= $page ?>">[이전]</a>
<?php else: ?>[이전]<?php endif; ?>
<?php if ($nextRow): ?>
<a href="read.php?id=<?= $nextRow['id'] ?>&page=<?= $page ?>">[다음]</a>
<?php else: ?>[다음]<?php endif; ?>
</td>
</tr>
</table>
</body>
</html>
글 수정 (edit.php / update.php)
edit.php — 수정 폼
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_GET['id'] ?? 0);
$page = (int) ($_GET['page'] ?? 1);
$stmt = $pdo->prepare("SELECT * FROM board WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row) exit('존재하지 않는 글입니다.');
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>글 수정</title></head>
<body>
<form action="update.php" method="post">
<input type="hidden" name="id" value="<?= $id ?>">
<input type="hidden" name="page" value="<?= $page ?>">
<table border="1" width="580">
<tr><td colspan="2" align="center"><b>글 수 정</b></td></tr>
<tr>
<td width="80">이름</td>
<td><input type="text" name="name" value="<?= h($row['name']) ?>" required></td>
</tr>
<tr>
<td>이메일</td>
<td><input type="text" name="email" value="<?= h($row['email']) ?>"></td>
</tr>
<tr>
<td>비밀번호</td>
<td><input type="password" name="pass" required> <small>(확인용)</small></td>
</tr>
<tr>
<td>제목</td>
<td><input type="text" name="title" size="60" value="<?= h($row['title']) ?>" required></td>
</tr>
<tr>
<td>내용</td>
<td><textarea name="content" cols="65" rows="15" required><?= h($row['content']) ?></textarea></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="저장">
<input type="button" value="취소" onclick="history.back()">
</td>
</tr>
</table>
</form>
</body>
</html>
update.php — 수정 처리
<?php
declare(strict_types=1);
require 'db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: list.php');
exit;
}
$id = (int) ($_POST['id'] ?? 0);
$page = (int) ($_POST['page'] ?? 1);
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$pass = trim($_POST['pass'] ?? '');
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$stmt = $pdo->prepare("SELECT pass FROM board WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row || !password_verify($pass, $row['pass'])) {
exit('비밀번호가 틀렸습니다.');
}
$pdo->prepare(
"UPDATE board SET name=:name, email=:email, title=:title, content=:content WHERE id=:id"
)->execute([
':name' => $name,
':email' => $email,
':title' => $title,
':content' => $content,
':id' => $id,
]);
header("Location: read.php?id={$id}&page={$page}");
exit;
글 삭제 (delete.php)
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_GET['id'] ?? 0);
$page = (int) ($_GET['page'] ?? 1);
if ($id === 0) {
header('Location: list.php');
exit;
}
// 비밀번호 확인 폼
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>삭제 확인</title></head>
<body>
<form method="post" action="delete.php?id=<?= $id ?>&page=<?= $page ?>">
비밀번호: <input type="password" name="pass" required>
<input type="submit" value="확인">
<input type="button" value="취소" onclick="history.back()">
</form>
</body>
</html>
<?php
exit;
}
// 비밀번호 검증 후 삭제
$stmt = $pdo->prepare("SELECT pass FROM board WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row || !password_verify($_POST['pass'], $row['pass'])) {
exit('비밀번호가 틀렸습니다.');
}
$pdo->prepare("DELETE FROM board WHERE id = :id")->execute([':id' => $id]);
header("Location: list.php?page={$page}");
exit;
파일 구조 정리
board/
├── db.php — DB 연결
├── list.php — 글 목록
├── write.php — 글쓰기 폼
├── insert.php — 글 저장
├── read.php — 글 읽기
├── edit.php — 수정 폼
├── update.php — 수정 처리
└── delete.php — 삭제
방명록을 만들어봤다면 게시판은 사실 그리 어렵지 않습니다.
제목과 조회수, 이전/다음 글 네비게이션이 추가됐을 뿐입니다.
CRUD 흐름이 손에 익으면, 어떤 데이터를 다루든 같은 방식으로 접근할 수 있습니다.