[WEB] PHP 게시판 페이징, 페이지네이션

Profile-exe·2021년 8월 21일
0

web

목록 보기
9/11
post-thumbnail

게시판의 글이 계속 생성되면, 스크롤이 끝없이 내려갈 것이다. 이를 방지하기 위해 한 페이지에 표시할 글의 개수를 정하여 일정 간격으로 페이지를 만드려고 한다.

이때, JavaScript에서 display:none;으로 자신이 쓴 글만 필터링해서 보여주는 코드가 있었는데, 페이지 네이션을 하니까 불필요한 페이지도 생기고 페이지가 바뀔때마다 필터링도 꺼졌다.

이 현상을 고치기 위해 두가지 방법을 사용했다.

  1. AJAX - 필터링 on/off 시에 필요한 데이터만 받아와서 표시
  2. 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

이 코드들을 참고하기를 바란다.

게시판 프로젝트 링크 - Github

프로젝트 디렉토리 구조 일부

classes 폴더
...
└── pagination.class.php

lib 폴더
...
└── delete_parameter.php

js 폴더
...
├── manage_cookie.js
└── my_board_list.js

manage_board 폴더
...
└── process_board_list.php


Paging & Pagination

둘의 차이에 대해 알아보았는데, 잘 설명된 글이 있었다.
해당 블로그

  • paging - 자원을 보존하기 위해 데이터베이스에서 항목의 한 페이지를 로드하는 작업
    ex) SQL 및 Model단
  • pagination은 사용자가 다음에 로드 할 페이지를 선택할 수 있도록 일련의 페이지 번호를 제공하는 UI 요소
    ex 각종 버튼들 : << 이전 1 2 3 4 5 다음 >>

paging을 이용해 pagination을 완성한다고 생각하면 되겠다.


1. 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 형태로 저장하고 반환한다. 이 stringAJAX이용 시 응답 데이터로 반환해주고, JavaScript에서 innerHTML에 넣어주는 식으로 화면에 표시할 것이다.
    부트스트랩pagination CSS가 적용되게 작성했다.

  • parameter를 지워주는 함수 - delete_parameter()

    URL에 존재하는 특정 값을 지운다. page가 여러개 붙지 않게 제거해주는 역할이다.
    /lib/delete_parameter.php에 존재한다. require_once로 가져와서 사용


2. 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_idxpage_set으로 쿼리문의 OFFSET, LIMIT을 결정한다.

  • 데이터 생성

    필터링 유무로 결정(WHERE 유무)되는 쿼리문의 결과를 받아와서 글 목록을 $topic_list로 만들어둔다. 또한, 페이지네이션 객체에서 nav를 생성하는 BottomPageNumber() 함수를 호출하여 해당 string을 저장한다.

  • 응답 내용 저장 - $response

    연관배열을 사용하여 해당 key이름에 맞게 저장 후 json_encode()echo 하여 응답(response)을 해준다.


3. 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.jsset_cookie()사용
    • unchecked - 필터링을 안하므로 아이디를 전달해줄 필요가 없다.
      • 필터링 유무 변경 - is_my_board = false
      • 쿠키 제거 - 수명을 0으로 하면 쿠키가 제거된다
  • AJAX로 데이터 받아서 화면에 뿌려주기

    사용자 아이디, 필터링 유무 등의 필요한 정보는 body 에 저장해두었으므로 init객체의 body필드에다가 JSON.stringify()를 이용하여 저장 후 요청한다.

    echo로 응답했으므로 text()로 받은 후 JSON.parse()를 사용해 일반 객체로 만들어준다.

    이후 연관배열 형태의 key들이 객체의 필드 이름이 되었으므로 HTML에 뿌릴 위치를 getElementById()로 가지고와서 innerHTML객체['필드 이름']을 넣어준다.


4. 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로 가져올 수 있도록 했다.


정리

이번에 페이지네이션과 필터링 기능을 구현하며 많은 시행착오를 겪었다. 거의 하루를 날려버렸는데, 이렇게 뻘짓을 좀 하고 해결한 코드를 보고 나니 머릿속으로 대강 정리가 되었다.

  • JavaScriptDOM 조작이 가능하므로 HTML쪽에서 값을 넘겨주고 싶으면 태그에서 value등의 어트리뷰트에 담기

  • 응답 내용을 명시적으로 하고 싶으면 연관배열을 사용해 JSON형태로 반환하기

  • AJAX로 값을 넘겨주는 데이터는 request body 전용 객체를 만들어서 값을 저장한 후 JSON.stringify()를 이용해 string형태로 넘겨주기

  • PHP와 달리 JavaScript에서는 Cookie 수명 기준이 ms 단위였음.. (1000 곱해주기)

동아리에서 웹 강의를 듣고 생활 코딩 강의도 쭉 돌려보고나니 직접 무언가를 만들어 보는 편이 좋을 것 같다고 생각해 시작하게 된 게시판 만들기였다.

이제 검색 기능 구현? 정도 남았는데, 구현 할지 안할지는 좀 고민이 된다. 현재 포스트 기준 88번의 Commit을 했는데, 생각했던 기능들은 거의 구현한 것 같다.

다음 프로젝트는 프레임워크를 이용해서 진행해보려 한다. node js 또는 django인데 고민 좀 해봐야겠다.


게시판 프로젝트 URL

Github - Profile-exe/CRUDboardsite

profile
컴퓨터공학과 학부생

0개의 댓글