게시판의 글이 계속 생성되면, 스크롤이 끝없이 내려갈 것이다. 이를 방지하기 위해 한 페이지에 표시할 글의 개수를 정하여 일정 간격으로 페이지를 만드려고 한다.
이때, 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
에 넣어주는 식으로 화면에 표시할 것이다.
부트스트랩의pagination
CSS가 적용되게 작성했다.
parameter
를 지워주는 함수 - delete_parameter()
URL
에 존재하는 특정 값을 지운다.page
가 여러개 붙지 않게 제거해주는 역할이다.
/lib/delete_parameter.php
에 존재한다.require_once
로 가져와서 사용
process_board_list.php
JavaScript
파일에서 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.js
AJAX
를 이용하여 데이터를 전달받고 이를 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_id
page_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
인데 고민 좀 해봐야겠다.