본문 바로가기
개발/Spring

[SpringBoot] React + TypeScript + JPA 프로젝트 (6) - 게시판 만들기(페이징 처리)

by 코딩하는 흰둥이 2024. 11. 7.

이전글

https://greed-yb.tistory.com/308

 

[SpringBoot] React + TypeScript + JPA 프로젝트 (5) - 게시판 만들기(목록)

이전글https://greed-yb.tistory.com/307 [SpringBoot] React + TypeScript + JPA 프로젝트 (4) - 회원가입, 로그인 하기이전글https://greed-yb.tistory.com/305 [SpringBoot] React + TypeScript + JPA 프로젝트 (3) - SpringSecurity 적용이

greed-yb.tistory.com

 

 

 

pagination.tsx
import {useEffect, useState} from "react";
import styled from "styled-components";

interface PagingProps {
    currentPage: number;                        // 현재 페이지
    pageItemSize: number;                       // 한 페이지에 나오는 데이터 수
    totalData: number;                          // 총 페이지 수
    clickPage: (page: number) => void;          // 클릭 한 페이지
    pageSize:number;                            // 페이지 bar에 보여질 페이지 개수
}

export const Paging = ({
                           currentPage,
                           pageItemSize,
                           totalData,
                           clickPage,
                           pageSize
                       }: PagingProps) => {

    // 총 페이지 개수
    const totalLength = (Math.ceil(totalData / pageItemSize)) < 1 ? 1 : (Math.ceil(totalData / pageItemSize));

    // 총 페이지 개수를 배열로 만든다
    const pagingArray = Array.from({length: totalLength},(item, index) => index);

    // pageSize 가 5 라고 했을때
    // [1,2,3,4,5] = 1 , [6,7,8,9,10] = 2 페이지 그룹번호 설정
    const [pageGroup, setPageGroup] = useState(1);

    useEffect(() => {
        setPageGroup(Math.floor((currentPage + pageSize - 1) / pageSize));
    }, [currentPage]);

    // 현재 페이지(setPageGroup)를 기준으로 페이지bar의 페이지 번호 설정하기
    const pageBarNumber =
        pageGroup === 1 ? Array.from({ length: pageSize },(item, index) => { return index; })
            : Array.from({length: pageSize },(item, index) => { return index + pageSize * (pageGroup - 1); });
    return (
        <PagingStyle>
            <li
                onClick={() => {
                    if (currentPage !== 1) {
                        clickPage(currentPage - 1);
                    }
                }}
                style={currentPage === 1 ? { backgroundColor: "#e7cfcf" } : null || undefined}
            >
                {'<'}
            </li>
            {pagingArray.map((item, index) => {
                if (pageBarNumber[0] <= index && index <= pageBarNumber[pageBarNumber.length - 1] ) {
                    if (index + 1 === currentPage) {
                        return (
                            <li key={item} id={(index + 1).toString()} className="currentPage" onClick={() => {clickPage(index + 1);}}>
                                {index + 1}
                            </li>
                        );
                    } else {
                        return (
                            <li key={item} id={(index + 1).toString()} onClick={() => { clickPage(index + 1); }} >
                                {index + 1}
                            </li>
                        );
                    }
                }
            })}
            <li onClick={() => {
                    if (currentPage !== pagingArray.length) {
                        clickPage(currentPage + 1);
                    }
                }}
                style={ currentPage === pagingArray.length ? { backgroundColor: "#e7cfcf" } : null || undefined } >
                {'>'}
            </li>
        </PagingStyle>
    );
};


const PagingStyle = styled.ul`
  margin: auto;
  width: 300px;
  display: flex;
  justify-content: center;
  justify-content: space-evenly;
  align-items: center;

  & ul, li {
    list-style: none;
  }

  & li {
    width: 35px;
    height: 35px;
    text-align: center;
    font-size: 25px;
    border-radius: 5px;
    background-color: rgb(140, 218, 152);
    transition: 0.6s;
    cursor: pointer;

    &:hover {
      color: white;
      background-color: rgb(140, 218, 152);
    }
  }

  & .currentPage {
    color: white;
    background-color: rgb(140, 218, 152);
  }

  & li:first-child, li:last-child {
    background-color: rgb(240, 248, 39);

    &:hover {
      background-color: rgb(140, 218, 152);
    }
  }
`;

 

 

 

 

board.tsx
import React, {useEffect, useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";
import {Paging} from "../utils/pagination";


// 게시판 변수 타입 설정
interface Board {
    no: number;
    title: string;
    writer: string;
    created: string;
}

function Board() {
    // 게시판 목록 변수
    const [BoardList, setBoardList] = useState<Board[]>([]);
    const [boardDataSet, setBoardDataSet] = useState<Board[]>([]);

    // 검색 변수
    const [searchOption, setSearchOption] = useState('title');
    const [search, setSearch] = useState('');

    // enter key 적용
    const enterKey = (event: React.KeyboardEvent<HTMLInputElement>) =>{
        if(event.key === "Enter"){
            axiosBoardList();
        }
    }

    useEffect(() => {
        axiosBoardList();
    }, []);

    // 게시물 목록 가져오는 axios
    const axiosBoardList = async () => {
        await axios.get("/api/boardList" , {
            params: {
                searchOption,
                search,
            },
        }).then((response) => {
            if(response.data.length > 0){
                setBoardList(response.data);
                setPage(1);
            }else{
                // 게시글 변수 초기화
                setBoardList([]);
                setBoardDataSet([]);
                setPage(1);
            }
        }).catch((error) => {
            console.log("error : " + error);
        });
    }


    // 현재 페이지 설정
    const [page, setPage] = useState(0);
    const handlePageChange = (page: number) => {
        setPage(page);
    };

    // 한 페이지에서 나오는 게시글 수
    const pageItemSize : number = 5;

    // pageItemSize 을 가지고 배열을 만들어준다
    let indexArray = Array.from({ length: pageItemSize }, (item, index) => {
        return index;
    });

    let pageIndex : number[] = [];
    // 현재 페이지 번호에 맞는 게시글 번호를 담는다
    // pageItemSize = 5 로 설정한 상태
    // 1page = [0,1,2,3,4] / 2page = [5,6,7,8,9] ....
    pageIndex = page === 1 ? indexArray : indexArray.map((item) => item + (page - 1) * pageItemSize);

    // 현재 페이지의 게시글 데이터
    const pagingData: Board[] = [];

    // 검색 및 페이징에 따른 데이터 변경
    const dataChange = () => {
        if(BoardList.length > 0){
            for (let i = 0; i < indexArray.length; i++) {
                if (BoardList && BoardList[pageIndex[i]] === undefined) {
                    break;
                } else {
                    pagingData.push(BoardList[pageIndex[i]]);
                }
            }
            setBoardDataSet(pagingData);
        }else{
            setBoardDataSet([]);
        }
    };

    // 게시물 변수 및 페이지 클릭 시 게시글 데이터 변경
    useEffect(() => {
        if (BoardList.length > 0) {
            dataChange();
        }
    }, [BoardList ,page]);


    return (
        <main id="main" className="main">
            <div className="pagetitle">
                <h1>Data Tables</h1>
                <nav>
                    <ol className="breadcrumb">
                        <li className="breadcrumb-item"><Link to="/">Home</Link></li>
                        <li className="breadcrumb-item">Tables</li>
                        <li className="breadcrumb-item active">Data</li>
                    </ol>
                </nav>
            </div>
            <section className="section">
                <div className="row">
                    <div className="col-lg-12">

                        <div className="card">
                            <div className="card-body">
                                <h5 className="card-title">게시판 목록</h5>
                                <p>Add lightweight datatables to your project with using the <a
                                    href="https://github.com/fiduswriter/Simple-DataTables" target="_blank">Simple
                                    DataTables</a> library. Just add <code>.datatable</code> class name to any table you
                                    wish to conver to a datatable. Check for <a
                                        href="https://fiduswriter.github.io/simple-datatables/demos/" target="_blank">more
                                        examples</a>.</p>

                                <table className="table datatable">
                                    <thead>
                                    <tr>
                                        <th>
                                            No
                                        </th>
                                        <th>Title</th>
                                        <th>Writer</th>
                                        <th data-type="date" data-format="YYYY/DD/MM">Created</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    {boardDataSet.length > 0 ? (
                                        boardDataSet.map((board) => (
                                            <tr key={board.no}>
                                                <td>{board.no}</td>
                                                <td>{board.title}</td>
                                                <td>{board.writer}</td>
                                                <td>{board.created}</td>
                                            </tr>
                                        ))
                                    ) : (
                                        <tr>
                                            <td colSpan={4} style={{ textAlign: 'center' }}>데이터가 없습니다.</td>
                                        </tr>
                                    )}
                                    </tbody>
                                </table>
                                <div className="search-bar" style={{textAlign: "center"}}>
                                    <select onChange={(event => setSearchOption(event.target.value))}>
                                        <option value="title">제목</option>
                                        <option value="writer">작성자</option>
                                    </select>
                                    <input type="text" name="query" placeholder="Search" title="Enter search keyword" onChange={event => setSearch(event.target.value)} onKeyDown={enterKey}/>
                                    <button type="button" title="Search" className="btn btn-light" onClick={axiosBoardList}>검색</button>
                                    <Paging
                                        currentPage={page}
                                        pageItemSize={pageItemSize}
                                        totalData={BoardList.length}
                                        clickPage={handlePageChange}
                                        pageSize={10}
                                    />

                                </div>
                                </div>
                        </div>

                    </div>
                </div>
            </section>

        </main>

    );
}

export default Board;

 

 

 

 

BoardVo
import jakarta.persistence.*;
import lombok.Data;

@Data
@Entity
@Table(name = "Board")
public class BoardVo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 값 자동 생성
    private Long no;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String writer;

    @Column(nullable = false)
    private String contents;

    @Column(nullable = false)
    private String created;
}

 

 

 

BoardRepository
import com.example.reactpractice.vo.boardVo.BoardVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BoardRepository extends JpaRepository<BoardVo , Long> {
    @Query("SELECT a FROM BoardVo a WHERE" +
            "(:searchOption = 'title' AND a.title LIKE %:search%) OR " +
            "(:searchOption = 'writer' AND a.writer LIKE %:search% )" +
            "ORDER BY a.no DESC")
    List<BoardVo> search(@Param("searchOption") String searchOption , @Param("search") String search);
}

 

 

DB Table
-- 테이블 생성        
CREATE TABLE BOARD(
	NO			NUMBER PRIMARY KEY,
	TITLE			VARCHAR2(50),
	WRITER		VARCHAR2(20),
	CONTENTS 	CLOB,
	CREATED		DATE DEFAULT SYSDATE
)



-- 임시 데이터
INSERT INTO board(NO, TITLE, WRITER, CONTENTS)VALUES(
	(SELECT NVL(MAX(NO) , 0) + 1 AS NO FROM BOARD ),
	'test',
	'글쓴이',
	'공백'
	)

 

 

 

BoardController
import com.example.reactpractice.repository.board.BoardRepository;
import com.example.reactpractice.vo.boardVo.BoardVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/*")
public class BoardController {

    @Autowired
    private BoardRepository boardRepository;

    @GetMapping("/boardList")
    public List<BoardVo> boardList(@RequestParam String searchOption , @RequestParam String search) throws Exception{

        List<BoardVo> vo = boardRepository.search(searchOption, search);
        return vo;
    }
}

 

 

 

 

TEST

정상적으로 나오고 있음

 

 

 

작성자로 변경하여 검색했을 때

 

 

데이터가 없을 때

 

 

 

 


React + Paging 예제를 구글링 하다가

페이지 bar 에 styled-components 를 사용하는 글을 보게 되었고

그 분 글을 중심으로 나에게 맞춰서 코드를 변경하였다 

 

select 와 input 태그 등을 추가하였고 변수나 기능을 추가 및 삭제하였다

 

React 에 대한 기초가 없는 상태에서 

html 에서 사용하던 대로 style 이나 javascript 를 사용하니

사용하는 족족 빨간 줄에러를 볼 수 있었고...

찾고 찾아 제대로 작성하면 이번엔 typescript 문법에 안 맞아서 빨간 줄과 에러를 볼 수 있었다

 

예전에 vue 를 사용할 때 도 watch  때문에 고생했던 거 같은데

이번엔 Hook(useEffect, useState) 때문에 엄청 고생했다

 

소스코드 외에 따로 설명은 하지 않겠다

주석으로도 충분하며, 코드를 직접 돌려보고 부딪혀 보길 바란다

 

 

데이터를 모두 가져와서 Paging 처리하는 방식을 개인적으로 좋아하지 않는데

예제가 많았고 React 를 공부할 겸 구현해 보았다

 

어느 정도 프로젝트가 끝나면 한 페이지마다

데이터를 불러오는 식으로 Paging 을 변경해야겠다

 

댓글