Spring Boot/STUDY

[Spring Boot] React연동 프로젝트(2) - 페이징 기능 구현

코맹 2024. 7. 3. 16:09

🔽이전 글

 

[Spring Boot] React연동 프로젝트

프로젝트 실행하기 위해 Spring Boot 웹서버, React 프론트 웹 서버도 함께 실행시킨다.  1. 리액트 프로젝트 생성- /spring03/frontboard 폴더 생성cd spring03npx create-react-app frontboard  2. 리액트 라이브러

iieunji023.tistory.com

 

 

 

💥💥💥

Spring Boot 서버가 실행되지 않았을 때 프론트 서버부터 시작하면 Uncaught runtime error가 발생한다.

axios request가 예외발생시키기 때문에 try-catch로 wrapping 해줘야 한다.

 

BoardList.js  예외처리 로직 추가
  const getBoardList = async () => {
    var pageString = 'page=0';

    try{  // 백엔드 서버가 실행되지 않으면 예외발생. AXIOS ERROR
      const resp = (await axios.get("//localhost:8080/api/board/list/free?" + pageString));

      if(resp.status === 200) {
        setBoardList(resp.data);  // boardList에 데이터가 들어감
        console.log(resp.data);

      } else if(resp.status === 404) {
        alert("서버 페이지가 없습니다.");

      } else if(resp.status === 500) {
        alert("서버 오류입니다.");

      }
    }catch(error) {
      console.log(">>>>> " + error);
      alert("서버가 연결되지 않았습니다.");

    }
  }
  • 콘솔창에 axios error가 뜨는 것을 확인할 수 있음

 

 

 

그럼 이제 페이징 기능을 구현해보겠다!

 

(Backend) /dto/PagingDto.java 생성
package com.eunji.backboard.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PagingDto {
  private int pageSize;       // 화면당 보여지는 게시글 최대개수
  private int totalPageNum;   // 총 페이지 수
  private long totalListSize; // 총 게시글 수

  private int page;           // 현재 페이지
  private int startPage;      // 시작 페이지 번호
  private int endPage;        // 마지막 페이지 번호
  
  private int startIndex;     // 시작 인덱스 번호

  private int block;          // 현재 블럭(구간)
  private int totalBlockNum;  // 총 블럭 수
  private int prevBlock;      // 이전 블럭
  private int nextBlock;      // 다음 블럭

  /*
   * 전체 리스트 크기와 현재 페이지와 페이지마다 나타낼 글 개수, 블럭 수를 가지고
   * 필요 변수들의 값을 계산하는 생성자
   */
  public PagingDto(Long totalListSize, Integer page, Integer pageSize, Integer blockSize ) {
    this.pageSize = pageSize;
    this.page = page;
    this.totalListSize = totalListSize;
    // 변수 값 계산
    // 전체 블럭 수 계산
    this.totalPageNum = (int) Math.ceil(this.totalPageNum * 1.0 / this.pageSize);
    // 현재 블럭 계산
    this.block = (int) Math.ceil((this.page) * 1.0 / blockSize);
    // 한 블럭 시작페이지
    this.startPage = ((this.block - 1) * blockSize + 1);
    // 현재 블럭 마지막 페이지
    this.endPage = this.startPage + blockSize - 1;
    // 블럭 마지막 페이지 검증(한 블랙이 10페이지가 안넘으면 마지막 페이지를 최대 페이지 수로 다시 변경 10 -> 3)
    if(this.endPage > this.totalPageNum) this.endPage = this.totalBlockNum;
    // 이전 블럭(클릭 시, 이전 블럭 마지막 페이지)
    this.prevBlock = (this.block * blockSize) - blockSize;
    // 이전 블럭 검증
    if(this.prevBlock < 1) this.prevBlock = 1;    // 1페이지보다 작을 순 없음
    // 다음 블럭 
    this.nextBlock = (this.block * blockSize + 1);
    // 다음 블럭 검증
    if(this.nextBlock > this.totalPageNum) this.nextBlock = this.totalPageNum;    // 전체 페이지 수보다 블럭 수가 클 순 없음
    // 시작 인덱스 번호
    this.startIndex = (this.page - 1) * this.pageSize;

  }
}

 

 

(Backend) /dto/Header.java 생성
package com.eunji.backboard.dto;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Header<T> {
  private LocalDateTime transactionTime;    // json으로 전달한 시간(스프링부트 -> 리액트)
  private String resultCode;    // 트랜잭션이 성공인지 실패인지 알려줌
  private String description;
  private T data; // 실제 게시글 데이터 담는 곳
  private PagingDto paging;

  @SuppressWarnings("unchecked")
  public static <T> Header<T> OK() {
    return (Header<T>) Header.builder()
                             .transactionTime(LocalDateTime.now())
                             .resultCode("OK")
                             .description("NO ERROR")
                             .build();
  }

  @SuppressWarnings("unchecked")
  public static <T> Header<T> OK(T data) {
    return (Header<T>) Header.builder()
                             .transactionTime(LocalDateTime.now())
                             .resultCode("OK")
                             .description("NO ERROR")
                             .data(data)
                             .build();
  }

  @SuppressWarnings("unchecked")
  public static <T> Header<T> OK(T data, PagingDto paging) {
    return (Header<T>) Header.builder()
                             .transactionTime(LocalDateTime.now())
                             .resultCode("OK")
                             .description("NO ERROR")
                             .data(data)
                             .paging(paging)
                             .build();
  }

  @SuppressWarnings("unchecked")
  public static <T> Header<T> OK(String description) {
    return (Header<T>) Header.builder()
                             .transactionTime(LocalDateTime.now())
                             .resultCode("ERROR")
                             .description(description)
                             .build();
  }

}

 

RestBoardController.java list() 수정
List<BoardDto> -> Header<List<BoardDto>>로 형 변환
  • RestBoardController.java 변경된 코드
public Header<List<BoardDto>> list(@PathVariable(value="category") String category,
                     @RequestParam(value="page", defaultValue = "0") int page,
                     @RequestParam(value = "kw", defaultValue = "") String keyword) {
          
    PagingDto paging = new PagingDto(pages.getTotalElements(), pages.getNumber() + 1, 10, 10);
    
    // Header<>에 담아줌
    Header<List<BoardDto>> last = Header.OK(result, paging);
    return last;
          
   }

 

 

(Front) BoardList.js 변수 부분 수정
  • BoardList.js
  const getBoardList = async () => {
    var pageString = 'page=0';

    try{  // 백엔드 서버가 실행되지 않으면 예외발생. AXIOS ERROR
      const resp = (await axios.get("//localhost:8080/api/board/list/free?" + pageString));
      
      const resultCode = resp.data.resultCode;  // header가 잘못된 경우 진행되면 안됨
      console.log(resultCode);

        setBoardList(resp.data.data);  // boardList 변수에 담는 작업
        const paging = resp.data.paging;
        console.log(resp.data.data);
        console.log(paging);

    }catch(error) {
      console.log(">>>>> " + error);
      alert("서버가 연결되지 않았습니다.");

    }
  }

 

  • resultCode = OK
  • resp.data.data = 게시글 Array()
  • paging = 페이지 object
  • BoardList.js
import axios from 'axios';  // REST API 호출 핵심!!

// Hook함수 사용
import React, { useState, useEffect } from 'react';

// Navigation
import { Link } from 'react-router-dom';

function BoardList() {    // 객체를 만드는 함수
  // 변수 선언
  // return{} | render() html, react 태그에서 반복할 때 사용됨
  const [boardList, setBoardList] = useState([]); // 배열값을 받아서 상태를 저장하기 때문에 []
  const [pageList, setPageList] = useState([]);   // 페이징을 위한 배열데이터
  const [nextBlock, setNextBlock] = useState(0);  // 다음 블럭 값
  const [prevBlock, setPrevBlock] = useState(0);  // 이전 블럭 값
  const [lastPage, setLastPage] = useState(0);    // 마지막 페이지 번호
  
  // 함수선언
  // 제일 중요!!
  const getBoardList = async (page) => {
    var pageString = (page == null) ? 'page=0' : 'page=' + page;

    try{  // 백엔드 서버가 실행되지 않으면 예외발생. AXIOS ERROR
      const resp = (await axios.get("//localhost:8080/api/board/list/free?" + pageString));
      
      const resultCode = resp.data.resultCode;  // header가 잘못된 경우 진행되면 안됨
      // console.log(resultCode);   // OK or ERROR

      if(resultCode == 'OK') {
        setBoardList(resp.data.data);  // boardList 변수에 담는 작업
        const paging = resp.data.paging;
        // console.log(resp.data.data);
        console.log(paging);    // 개발이 완료되면 콘솔로그는 주석처리할 것

        const { endPage, nextBlock, page, prevBlock, startPage, totalListSize, totalPageNum } = paging;
        console.log(totalListSize);
        console.log(totalPageNum);

        const tmpPages = [];
        for (let i = startPage; i <= endPage; i++) {
          tmpPages.push(i);   // [1, 2, 3, 4, ...]

        }
        setPageList(tmpPages);
        setNextBlock(nextBlock);
        setPrevBlock(prevBlock);
        setLastPage(totalPageNum);

      } else {
        alert("문제가 발생하였습니다");
      
      }
    }catch(error) {
      console.log(">>>>> " + error);
      alert("서버가 연결되지 않았습니다.");

    }
  }

  // onPageClick() - 페이지 번호 클릭시
  function onPageClick(page){
    console.log(page);
    getBoardList(page - 1);   // Spring Boot에서는 0부터 시작했기 때문에 -1을 해줘야 함
  }

  useEffect(() => {
    getBoardList();
  }, []); // 값이 없을때는 빈 화면

  return (
    <div className='container'>
      <table className='table'>
        <thead className='table-dark'>
          <tr className='text-center'>
            <th>번호</th>
            <th style={{width: '50%'}}>제목</th>
            <th>작성자</th>
            <th>조회수</th>
            <th>작성일</th>
          </tr>
        </thead>
        <tbody>
          {/* 반복으로 들어갈 부분 */}
          {boardList.map((board) => (
          <tr className='text-center' key={board.bno}>
            <td>{board.bno}</td>
            <td className='text-start'>{board.title}</td>
            <td>{board.writer}</td>
            <td>{board.hit}</td>
            <td>{board.createDate}</td>
          </tr>
        ))}
        </tbody>
      </table>
      {/* 페이징 처리 */}
      <div className='d-flex justify-content-center'>
        <nav aria-label='Page navigation'>
          <ul className='pagination'>
            <li>
              <button className='page-link' aria-label='first' onClick={() => onPageClick(1)}>
                <span>&laquo;</span>
              </button>
            </li>
            <li className='page-item'>
              <button className='page-link' aria-label='previous' onClick={() => onPageClick(prevBlock)}>
                <span>&lt;</span>
              </button>
            </li>
            {pageList.map((page, index) => (
              <li className='page-item' key={index}>
                <button className='page-link' onClick={() => onPageClick(page)}>
                  {page}
                </button>
              </li>
            ))}
            <li className='page-item'>
              <button className='page-link' aria-label='next' onClick={() => onPageClick(nextBlock)}>
                <span>&gt;</span>
              </button>
            </li>
            <li>
              <button className='page-link' aria-label='first' onClick={() => onPageClick(lastPage)}>
                <span>&raquo;</span>
              </button>
            </li>
          </ul>
        </nav>
      </div>
    </div>
  );
}

export default BoardList;

 


작성일 출력 변경

 

(front) /common/CommonFunc.js 생성 
- 작성일 수정하기 위한 함수 formatDate() 작성

  • 작성일이 초 단위까지 나오는 문제(?) 발생
  • 따라서 CommonFunc.js에서 작성일을 변경

/common/CommonFunc.js

 

  • CommonFunc.js
export function formatDate(date) {  // 2024-07-01T09:27:11.269634
  var result = date.replace('T', ' ');  // T를 공백으로 변경
  var index = result.lastIndexOf(' ');  // 초 앞에 있는 : 위치 값, ' '은 yyyy-mm-dd만 남김
  
  result = result.substr(0, index); // 초 뒤로 삭제
  return result;
  
}
  • 년월일만 나오도록 수정
  • BoardList.js에서 작성일 출력 코드 수정
<td>{common.formatDate(board.createDate)}</td>
  • CommonFunc.js에 작성한 formarDate() 메서드를 통해 변경해줌

변경된 것 확인

 

/BoardList.js 댓글 갯수 표시
<tbody>
          {/* 반복으로 들어갈 부분 */}
          {boardList.map((board) => (
          <tr className='text-center' key={board.bno}>
            <td>{board.bno}</td>
            <td className='text-start'>{board.title}&nbsp;
              {
                board.replyList != null &&
                <span class="badge text-bg-warning">{board.replyList.length}</span>
              }
            </td>
            <td>{board.writer}</td>
            <td>{board.hit}</td>
            <td>{common.formatDate(board.createDate)}</td>
          </tr>
        ))}
        </tbody>

 

댓글 갯수 표시된 것 확인

 

 


게시글 번호 변경

(Backend) /dto/BoardDto.java 게시글 번호 변수 추가
private Long num;

 

/restcontroller/RestBoardController.java 게시글 번호 계산 로직 추가
long curNum = pages.getTotalElements() - pages.getNumber() * 10;	// 게시글 번호

    for (Board origin : pages) {
      List<ReplyDto> subList = new ArrayList<>();

      BoardDto bdDto = new BoardDto();
      // 게시글 번호 추가
      bdDto.setNum(curNum--);   // 한번 돌때마다 1씩 빼줌
      
   	  ....
      
   }
  • 이제 프론트에서도 출력값을 bno가 아닌 num으로 바꿔줘야 함

 

/BoardList.js bno를 num으로 변경
 <tr className='text-center' key={board.bno}>