
게시판의 글이 계속 생성되면, 스크롤이 끝없이 내려갈 것이다. 이를 방지하기 위해 한 페이지에 표시할 글의 개수를 정하여 일정 간격으로 페이지를 만드려고 한다.
이때, JavaScript에서 display:none;으로 자신이 쓴 글만 필터링해서 보여주는 코드가 있었는데, 페이지 네이션을 하니까 불필요한 페이지도 생기고 페이지가 바뀔때마다 필터링도 꺼졌다.
이 현상을 고치기 위해 두가지 방법을 사용했다.
- AJAX - 필터링 on/off 시에 필요한 데이터만 받아와서 표시
- Cookie - 필터링 on/off 유지를 위해 페이지 갱신 시 쿠키 유무 확인 후 필터링이 유지되도록 처리
이번 포스트에서는 수시간의 뻘짓을 거쳐 완성된 글 필터링 & 페이지네이션 코드를 보여주겠다.
코드가 많아서 스크롤이 길긴 한데, 프로젝트 링크를 이곳이랑 맨 아래 걸어두니
/classes/pagination.class.php, /js/my_board_list.js, /js/manage_cookie.js, /manage_board/process_board_list.php, /lib/delete_parameter.php
이 코드들을 참고하기를 바란다.
classes 폴더
...
└── pagination.class.phplib 폴더
...
└── delete_parameter.phpjs 폴더
...
├── manage_cookie.js
└── my_board_list.jsmanage_board 폴더
...
└── process_board_list.php
Paging & Pagination둘의 차이에 대해 알아보았는데, 잘 설명된 글이 있었다.
해당 블로그
- paging - 자원을 보존하기 위해 데이터베이스에서 항목의 한 페이지를 로드하는 작업
ex) SQL 및 Model단
- pagination은 사용자가 다음에 로드 할 페이지를 선택할 수 있도록 일련의 페이지 번호를 제공하는 UI 요소
ex 각종 버튼들 : << 이전 1 2 3 4 5 다음 >>
즉 paging을 이용해 pagination을 완성한다고 생각하면 되겠다.
pagination 클래스어떤 방식을 적용할지 보다가 클래스로 변환하신 분이 있어서 이 코드를 가져와서 내 방식대로 고쳐보았다. 달라진 점은 이 블로그를 참고해라.
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/classes/db.class.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/lib/delete_parameter.php';
class Pagination {
//클래스 내부에서 하단 페이지넘버 처리에 필요한 변수
private
$page,
$total_page,
$first_page,
$last_page,
$prev_page,
$next_page,
$total_block,
$next_block,
$next_block_page,
$prev_block,
$prev_block_page,
$page_nav = "",
$PHP_SELF = "/index.php";
//설정에서 register_globals=Off 인 경우에 $PHP_SELF 수퍼변수는 동작하지 않기때문에 경로를 지정해주는것이 좋다.
//클래스 외부에서 필요한 변수
public
$limit_idx,
$page_set;
//페이지 줄수, 블럭수 받아 데이터 정리
public function __construct($page_count, $block_count, $page_num, $is_my_board) {
$block_set = $block_count; // 한페이지 블럭수
$this->page_set = $page_count; // 한페이지 줄수
$result = DB::query("SELECT count(board_id) AS total FROM board {$is_my_board}")[0];
$total = $result['total']; // 전체글수
$this->total_page = ceil($total / $this->page_set); // 총페이지수(올림함수)
$this->total_block = ceil($this->total_page / $block_set); // 총블럭수(올림함수)
$this->page = $page_num; // parameter로 현재 페이지정보를 받아옴
$block = ceil($this->page / $block_set); // 현재블럭(올림함수)
$this->limit_idx = ($this->page - 1) * $this->page_set; // limit 시작위치
$this->first_page = (($block - 1) * $block_set) + 1; // 첫번째 페이지번호
$this->last_page = min($this->total_page, $block * $block_set); // 마지막 페이지번호
$this->prev_page = $this->page - 1; // 이전페이지
$this->next_page = $this->page + 1; // 다음페이지
$this->prev_block = $block - 1; // 이전블럭
$this->next_block = $block + 1; // 다음블럭
// 이전블럭을 블럭의 마지막으로 하려면...
$this->prev_block_page = $this->prev_block * $block_set; // 이전블럭 페이지번호
// 이전블럭을 블럭의 첫페이지로 하려면...
//$prev_block_page = $prev_block * $block_set - ($block_set - 1);
$this->next_block_page = $this->next_block * $block_set - ($block_set - 1); // 다음블럭 페이지번호
}
//하단 페이지 넘버링
public function BottomPageNumber(): string {
$this->page_nav .= ($this->prev_page > 0) ?
"<li class='page-item'><a class='page-link' href='$this->PHP_SELF?page=$this->prev_page'>Prev</a></li>" :
"<li class='page-item disabled'><span class='page-link'>Prev</span></li>";
$this->page_nav .= ($this->prev_block > 0) ?
"<li class='page-item'><a class='page-link' href='$this->PHP_SELF?page=$this->prev_block_page'>...</a></li>" :
"<li class='page-item disabled'><span class='page-link'>...</span></li>";
for ($i = $this->first_page; $i <= $this->last_page; $i++) {
$this->page_nav .= ($i != $this->page) ?
"<li class='page-item'><a class='page-link' href='$this->PHP_SELF?page=$i'>$i</a></li>" :
"<li class='page-item disabled'><span class='page-link'>$i</span></li>";
}
$this->page_nav .= ($this->next_block <= $this->total_block) ?
"<li class='page-item'><a class='page-link' href='$this->PHP_SELF?page=$this->next_block_page'>...</a></li>" :
"<li class='page-item disabled'><span class='page-link'>...</span></li>";
$this->page_nav .= ($this->next_page <= $this->total_page) ?
"<li class='page-item'><a class='page-link' href='$this->PHP_SELF?page=$this->next_page'>Next</a></li>" :
"<li class='page-item disabled'><span class='page-link'>Next</span></li>";
// 페이지 파라미터 중복 제거
$this->PHP_SELF = delete_parameter($this->PHP_SELF, 'page');
return $this->page_nav;
}
}
생성자 - __construct()
한 페이지에 보여줄 줄 수(
$page_count), 네비게이션에 보여줄 블럭 수($block_count), 현재 페이지($page_num), 필터링 on/off 유무($is_my_board)를 받는다.
받은 정보를 바탕으로한 필요한 정보들을 필드에 저장해둔다.
네비게이션 생성 메소드 - BottomPageNumber()
뷰포트 하단에 표시할
nav의 내용인html 텍스트를string형태로 저장하고 반환한다. 이string을 AJAX이용 시 응답 데이터로 반환해주고,JavaScript에서innerHTML에 넣어주는 식으로 화면에 표시할 것이다.
부트스트랩의paginationCSS가 적용되게 작성했다.
parameter를 지워주는 함수 - delete_parameter()
URL에 존재하는 특정 값을 지운다.page가 여러개 붙지 않게 제거해주는 역할이다.
/lib/delete_parameter.php에 존재한다.require_once로 가져와서 사용
process_board_list.phpJavaScript 파일에서 JSON형태로 데이터를 넘겨주면, json_decode()를 통해 PHP 객체로 사용할 수 있다.
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/classes/db.class.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/classes/pagination.class.php';
// 세션 시작 - $_SESISON['user_id'] 사용을 위해.
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$post_data = json_decode(file_get_contents('php://input')); // JSON 형태로 받은 데이터 객체로 parsing
// 필터링 유무에 따라 WHERE 구문 삽입 유무로 쿼리문 결정
// WHERE이 삽입되면 자신이 작성한 글만 fetch
$my_board = $post_data->is_my_board ? "WHERE board.user_id = '{$_SESSION['user_id']}'\n" : '';
$sql = "
SELECT
board_id, board_title, user_name, date_format(created, '%m-%d %H:%i') as created, view_count
FROM board INNER JOIN user
ON board.user_id = user.user_id
";
$sql .= $my_board; // 만약 필터링 off인 경우 $my_board == '' 이므로 아무런 영향이 없다.
// 게시글 줄 수, 블럭 수
$pagination = new Pagination($post_data->page_count, 5, $post_data->page_num, $my_board);
// 페이지네이션을 위한 LIMIT
$sql .= trim("
ORDER BY created DESC LIMIT {$pagination->limit_idx}, {$pagination->page_set}
");
$result = DB::query($sql);
$topic_list = '';
if ($result) { // 글이 존재하는 경우 table-row로 생성
foreach ($result as $index => $row) {
$topic_list .= "
<tr style='cursor:pointer' onclick='location.href=\"/manage_board/board_read.php?id={$row['board_id']}\"'>
<th class='col-1 text-center' scope='row'>{$row['board_id']}</th>
<td class='col-7'>{$row['board_title']}</td>
<td class='col-1 text-center'>{$row['user_name']}</td>
<td class='col-2 text-center'>{$row['created']}</td>
<td class='col-1 text-center'>{$row['view_count']}</td>
</tr>
";
}
}
// table-row랑 pagenation의 nav를 반환할 것임
$response = array(
'topic_list' => $topic_list,
'page_nav' => $pagination->BottomPageNumber()
);
echo json_encode($response); // JSON으로 encoding 후 반환
필터링 유무 - $my_board
필터링 유무를
$my_board에 값을 넣어WHERE ~쿼리문을 넣을지 말지 결정한다. 필터링이 꺼져있으면$my_board는''이므로$sql .= $my_board에서 아무런 영향을 끼치지 않는다.
페이지네이션 객체 - $pagination
$pagination에 페이지네이션 객체를 생성한 후public필드인limit_idx와page_set으로 쿼리문의OFFSET,LIMIT을 결정한다.
데이터 생성
필터링 유무로 결정(WHERE 유무)되는 쿼리문의 결과를 받아와서 글 목록을
$topic_list로 만들어둔다. 또한, 페이지네이션 객체에서nav를 생성하는BottomPageNumber()함수를 호출하여 해당string을 저장한다.
응답 내용 저장 - $response
연관배열을 사용하여 해당
key이름에 맞게 저장 후json_encode()로echo하여 응답(response)을 해준다.
my_board_list.jsAJAX를 이용하여 데이터를 전달받고 이를 innerHTML에 넣어서 화면을 갱신한다.
쿠키 유무에 따라 필터링 스위치 작동 유무를 결정한다.
const url = new URL(window.location.href);
const urlParams = url.searchParams;
let page = urlParams.get('page');
if (page === null) page = '1';
const body = { // AJAX request body
id: '',
page_count: 10, // 한 페이지에 표시할 글의 수
page_num: page, // 현재 페이지
is_my_board: false // 필터링 on / off 유무
}
function my_board_checked() { // 페이지 변경 시 필터링 스위치 checked 확인
const board_switch = document.getElementById('my_boards_switch');
if (get_cookie('my_board_filter') !== '') { // 쿠키 유무로 스위치 on/off 확인
body.is_my_board = true;
board_switch.click(); // 스위치 활성화 되어 있으면 click()로 재활성화
} else {
body.is_my_board = false;
}
}
document.getElementById('my_boards_switch').addEventListener('click', (e) => {
// 체크 된 경우 true
if (e.target.checked) {
body.id = e.target.value;
body.is_my_board = true;
set_cookie('my_board_filter', true, 1); // 쿠키 생성
} else {
body.is_my_board = false;
set_cookie('my_board_filter', false, 0); // 쿠키 제거
}
get_board_list(body); // 목록 불러오기
});
function get_board_list(body) { // AJAX 이용
const init = {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify(body) // JSON 형태로 POST request
}
fetch('/manage_board/process_board_list.php', init)
.then((res) => res.text())
.then((data) => {
const response_obj = JSON.parse(data); // JSON 형태로 응답받고 parsing 후 사용
document.getElementById('board_list').innerHTML = response_obj['topic_list'];
document.getElementById('page-list').innerHTML = response_obj['page_nav'];
});
}
my_board_checked();
get_board_list(body);
현재 페이지 값 얻기 - page
URL객체를 이용하여urlParams.get('page')를 통해page파라미터를 얻어와서 저장한다.
request body - body
fetch API를 이용해 요청 시request body부분이다.
id- 로그인 시 사용자의user_idpage_count- 한 페이지에 표시할 글의 수page_num- 현재 페이지is_my_board- 필터링 유무
페이지 갱신 시 필터링 유지 - my_board_checked()
manage_cookie.js에 있는get_cookie로 쿠키를 얻어와서 필터링 유무 판별한다.
true-request body(body 변수)에 있는 필터링 유무(is_my_board)를true로 변경하고,switch를 활성화시킨다.false- 필터링 유무를false로 변경한다.switch는 페이지 갱신 시 기본적으로 비활성화 상태이므로 그대로 놔두면 된다.
스위치 클릭 유무에 따른 필터링 - switch의 이벤트 리스너
checked- 로그인을 한 경우만 활성화 가능하게 만들었다. 그러므로 사용자 사용자 아이디를 전달해준다.
- 사용자의 아이디 저장 -
id = e.target.value- 필터링 유무 변경 -
is_my_board = true- 쿠키 생성 -
manage_cookie.js의set_cookie()사용unchecked- 필터링을 안하므로 아이디를 전달해줄 필요가 없다.
- 필터링 유무 변경 -
is_my_board = false- 쿠키 제거 - 수명을 0으로 하면 쿠키가 제거된다
AJAX로 데이터 받아서 화면에 뿌려주기
사용자 아이디, 필터링 유무 등의 필요한 정보는
body에 저장해두었으므로init객체의body필드에다가JSON.stringify()를 이용하여 저장 후 요청한다.
echo로 응답했으므로text()로 받은 후JSON.parse()를 사용해 일반 객체로 만들어준다.이후 연관배열 형태의
key들이 객체의 필드 이름이 되었으므로HTML에 뿌릴 위치를getElementById()로 가지고와서innerHTML에객체['필드 이름']을 넣어준다.
index.php로그인을 하지 않은 경우 필터링 스위치를 disabled 해준다. 로그인을 해서 필터링을 작동시킬 수 있으면 input태그의 value에 $_SESSION['user_id']를 넣어서 JavaScript에서 사용할 수 있도록 해준다.
...은 생략을 뜻한다.
// 세션 시작
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
...
$my_boards_switch = '
<input type="checkbox" class="form-check-input" id="my_boards_switch" disabled>
<label for="my_boards_switch" class="form-check-label">내가 쓴 글</label>
';
// 로그인을 한 경우
if (isset($_SESSION['user_id']) && $_SESSION['user_id'] != '') {
...
$my_boards_switch = "
<input type='checkbox' value='\"{$_SESSION['user_id']}\"' class='form-check-input' id='my_boards_switch'>
<label for='my_boards_switch' class='form-check-label'>내가 쓴 글</label>
";
}
...
<div class="form-check form-switch">
<?=$my_boards_switch?>
</div>
...
<tbody id="board_list" >
</tbody>
...
<footer class="mt-5">
<nav id="nav-pagination" class="position-absolute bottom-0 start-50 translate-middle">
<ul id="page-list" class="pagination d-flex justify-content-center">
</ul>
</nav>
</footer>
$my_board_switch로그인을 안한 경우 기본 값으로
disabled가 들어가있다.로그인을 한 경우 사용자 아이디를
value에 넣어서(value='\"{$_SESSION['user_id']}\"')my_board_list.js에서e.target.value로 가져올 수 있도록 했다.
이번에 페이지네이션과 필터링 기능을 구현하며 많은 시행착오를 겪었다. 거의 하루를 날려버렸는데, 이렇게 뻘짓을 좀 하고 해결한 코드를 보고 나니 머릿속으로 대강 정리가 되었다.
JavaScript는 DOM 조작이 가능하므로 HTML쪽에서 값을 넘겨주고 싶으면 태그에서 value등의 어트리뷰트에 담기
응답 내용을 명시적으로 하고 싶으면 연관배열을 사용해 JSON형태로 반환하기
AJAX로 값을 넘겨주는 데이터는 request body 전용 객체를 만들어서 값을 저장한 후 JSON.stringify()를 이용해 string형태로 넘겨주기
PHP와 달리 JavaScript에서는 Cookie 수명 기준이 ms 단위였음.. (1000 곱해주기)
동아리에서 웹 강의를 듣고 생활 코딩 강의도 쭉 돌려보고나니 직접 무언가를 만들어 보는 편이 좋을 것 같다고 생각해 시작하게 된 게시판 만들기였다.
이제 검색 기능 구현? 정도 남았는데, 구현 할지 안할지는 좀 고민이 된다. 현재 포스트 기준 88번의 Commit을 했는데, 생각했던 기능들은 거의 구현한 것 같다.
다음 프로젝트는 프레임워크를 이용해서 진행해보려 한다. node js 또는 django인데 고민 좀 해봐야겠다.