SpringBoot에서는 JPA를 통해 페이징 처리를 쉽게 활용할 수 있습니다.
예제 소스는 TodoList-backend
소스를 참고하여 작성하였습니다.
controller단에서 파라미터로 받는 정보들
: @RequestParam : pageNo(🌟0부터 시작)
, pageSize(페이징 갯수)
, sortBy
또는 RequestDto 에서
받아서, Pageable 객체 를 생성해야 합니다.
그리고 이 Pageable 객체를 파라미터로 받아 JpaRepository의 목록 관련 findAll() 과 같은 메서드로
-> Page<Entity>
로 return 됩니다.
Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(sortBy).descending);
Page<TodoEntity> todoPage = todoRepository.findAll(pageable);
// or
Page<TodoResponse> todoDtoPage = todoRepository.findAll(pageable).map(this::mapToDto);
그리고 Service 내에서 Entity를 Dto로 변환해주어야 합니다.
이것은 stream() 내 map() 메서드와 entityToDto 메서드를 미리 만들어서 처리해주면 됩니다.
findAll(pagable); => Page<Entity> -> getContent(); ->List<Entity>
-> mapping(stream) -> List<Dto>
✅ Response DTO를 만들어줘야 하는 이유!(front단이 받아야할 Page정보 외에도 많이 있을 수 있기에!)
Page<Entity>
의 content
를 따로 List<PostResponse>
로 처리하는 것을 잊으면 안됩니다!
front단에 보내주는 Dto 필드에 Paging 내용을 넣어줄 것!
예를들어 이런 것들이 있습니다.
totalElements
totalPages
last
boolean
page - 현재 페이지 넘버
size - 페이지 사이즈
offset
- 최초 시작점
limit
- size랑 같음
totalElements - 데이터 로우 총 갯수
totalPages - 페이지들 총 갯수
PageResponse
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PageResponse {
private List<TodoResponse> content;
private int pageNo;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
}
TodoController
// Paging test
@GetMapping
public PageResponse readAllPaging(
@RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo,
@RequestParam(value = "pageSize", defaultValue = "3", required = false) int pageSize,
@RequestParam(value = "sortBy", defaultValue = "id", required = false) String sortBy
) {
log.info("Read Paging All");
return todoService.searchAllPaging(pageNo, pageSize, sortBy);
}
or
MemoController
@GetMapping("/memo")
public ResponseEntity<Page<MemoResponse>> list(
@PageableDefault(sort = "id", direction = Sort.Direction.DESC, size = 5)
Pageable pageable)
{
Page<MemoResponse> responsePage = memoService.list(pageable);
return new ResponseEntity<>(responsePage, HttpStatus.OK);
}
TodoService
@Override
public PageResponse searchAllPaging(int pageNo, int pageSize, String sortBy) {
// create Pageable instance
Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(sortBy).descending());
Page<TodoEntity> todoPage = todoRepository.findAll(pageable);
// .map()을 더 추가해서 바로 Page<TodoResponse> 값으로 시작할 수 있어!
// Page<TodoResponse> todoDtoPage = todoRepository.findAll(pageable).map(this::mapToDto);
List<TodoEntity> listTodos = todoPage.getContent();
List<TodoResponse> content = listTodos.stream().map(TodoEntity -> mapToDto(TodoEntity)).collect(Collectors.toList());
return PageResponse.builder()
.content(content) // todoDtoPage.getContent()
.pageNo(pageNo)
.pageSize(pageSize)
.totalElements(todoPage.getTotalElements())
.totalPages(todoPage.getTotalPages())
.last(todoPage.isLast())
.build();
}
1) PageResponse
2) Page<T>
@PageableDefault(sort = "id", direction = Sort.Direction.DESC, size = 5) Pageable pageable
// view에 보낼 페이징 코드
// @PageableDefault 로 파라미터 정리
@GetMapping("/list")
public String list(Model model,
@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<Board> list = null;
int nowPage = list.getPageable().getPageNumber() + 1;
int startPage = Math.max(nowPage - 4, 1);
int endPage = Math.min(nowPage + 5, list.getTotalPages());
model.addAttribute("list", list);
model.addAttribute("nowPage", nowPage);
model.addAttribute("startPage", startPage);
model.addAttribute("endPage", endPage);
return "board/list";
}
PageableRequestDto
public class PageableRequestDto extends PageRequestDto {
}
PageRequestDto
@Builder
@AllArgsConstructor
@Data
public class PageRequestDto {
private int page;
private int size;
private String type;
private String keyword;
public PageRequestDto() {
this.page = 1;
this.size = 10;
}
public Pageable getPageable(Sort sort){
return PageRequest.of(page -1, size, sort);
}
}
PageResponseDto
@Data
public class PageResponseDto<T> {
// 페이징 content
private List<T> content;
// 총 페이지 번호
private int totalPage;
// 현재 페이지 번호
private int page;
// 목록 사이즈
private int size;
// 게시글 총갯수
private long totalCount;
// 화면에서 시작 페이지 번호, 끝 페이지 번호
private int start, end;
// 이전, 다음 링크여부
private boolean prev, next;
// 화면에 보여줄 페이지 번호 목록 리스트
private List<Integer> pageList;
public PageResponseDto(Pageable pageable, Page<T> pageInfo) {
content = pageInfo.getContent();
totalPage = pageInfo.getTotalPages(); // 페이지 총 갯수
totalCount = pageInfo.getTotalElements(); // 로우 총 갯수
makePageList(pageable);
}
private void makePageList(Pageable pageable) {
this.page = pageable.getPageNumber() + 1; // 0부터 시작하므로 1을 추가
this.size = pageable.getPageSize(); // 한 페이지에 포함될 row 의 최대개수, ex) size = 10, 화면에 보여줄 항목개수가 10개다.
int tempEnd = (int) (Math.ceil(page / (double) size)) * size; // 현재 페이지를 기준으로 화면에 출력되어야 하는 마지막 페이지 번호를 우선 처리, ex) size = 5, page = 6이다! => tempEnd = 10 인것!
start = tempEnd - (size - 1); // start = 6 = 10 - (5 - 1)
end = Math.min(totalPage, tempEnd); // 실제 데이터가 부족한 경우를 위해 마지막 페이지 번호는 전체 데이터의 개수를 이용해서 다시 계산
// end = (6, 10) = 6
prev = start > 1; // 6 > 1 => true
next = totalPage > tempEnd; // 6 > 10 => false
pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList()); // 현재 보여줄 목록 ex) 마지막 페이지에 들어갔다. 6
}
}
PageResponseDto(빌더패턴)
@Slf4j
@Getter
@Builder
@ToString
public class PageResponseDto<T> {
// 페이징 content
private List<T> content;
//현재 페이지 번호
private int page;
//목록 사이즈
private int size;
// 게시글 총갯수
private long totalCount;
//시작 페이지 번호, 끝 페이지 번호
private int start, end;
//이전, 다음
private boolean prev, next;
public static <T> PageResponseDto<T> of(Page<T> pageInfo) {
Pageable pageable = pageInfo.getPageable();
int totalPage = pageInfo.getTotalPages();
int page = pageable.getPageNumber() + 1; // 0부터 시작하므로 1을 추가
int size = pageable.getPageSize();
int tempEnd = (int) (Math.ceil(page / (double) size)) * size;
int start = tempEnd - (size - 1);
int end = Math.min(totalPage, tempEnd);
boolean prev = start > 1;
boolean next = totalPage > tempEnd;
return PageResponseDto.<T>builder()
.content(pageInfo.getContent())
.page(page)
.size(size)
.totalCount(pageInfo.getTotalElements())
.start(start)
.end(end)
.prev(prev)
.next(next)
.build();
}
}
BoardController
@GetMapping("/api/board/list-page")
public PageResponseDto listPage(
PageRequestDto pageRequestDto
) {
log.info("listPage, pageRequestDto: {}", pageRequestDto);
// 페이징 조건 결합 전체조회
PageResponseDto BoardInfoPage = boardService.getAllPage(pageRequestDto);
log.info("listPage BoardInfoPage: {}", BoardInfoPage);
return BoardInfoPage;
}
✅ BoardRepsitory(
QuerydslPredicateExecutor<T>
상속)
booleanbuilder
파라미터를 받은 페이징 타입 메서드를 사용할 수 있다.
BoardRepository
public interface BoardRepository extends JpaRepository<Board, Long>,
QuerydslPredicateExecutor<Board> {
Optional<Board> findByIdAndMemberId(Long boardId, int memberId);
List<Board> findByMemberId(int memberId);
}
BoardService
@Transactional(readOnly = true)
@Override
public PageResponseDto getAllPage(PageRequestDto pageRequestDto) {
Pageable pageable = pageRequestDto.getPageable(Sort.by("id").descending());
BooleanBuilder booleanBuilder = getSearch(pageRequestDto);
Page<BoardInfo> boardInfoPage = boardRepository.findAll(booleanBuilder, pageable)
.map(BoardInfo::toInfo);
log.info("getAll boardInfoPage: {}", boardInfoPage);
return new PageResponseDto(pageable, boardInfoPage);
}
/**
* 검색 조건 서칭 - querydsl 사용, 페이징과는 상관없음.
* @return Boolean 조건 집합
*/
private BooleanBuilder getSearch(PageRequestDto pageRequestDto) {
String type = pageRequestDto.getType();
BooleanBuilder booleanBuilder = new BooleanBuilder();
QBoard qBoard = QBoard.board;
String keyword = pageRequestDto.getKeyword();
BooleanExpression expression = qBoard.id.gt(0L); // id > 0 조건만
booleanBuilder.and(expression);
// 검색 조건 없는 경우
if (StringUtils.isEmpty(type)) {
return booleanBuilder;
}
// 검색 조건이 있는 경우
BooleanBuilder conditionBuilder = new BooleanBuilder();
if(type.contains("t")){
conditionBuilder.or(qBoard.title.contains(keyword));
}
if(type.contains("c")){
conditionBuilder.or(qBoard.content.contains(keyword));
}
if(type.contains("w")){
conditionBuilder.or(qBoard.writer.contains(keyword));
}
//모든 조건 통합
booleanBuilder.and(conditionBuilder);
return booleanBuilder;
}
vue
<!-- pagination -->
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<template v-if="prev">
<li class="page-item">
<a class="page-link" href="javascript:;" @click="fnPage(1)" ><<</a>
<a class="page-link" href="javascript:;" @click="fnPage(`${start-1}`)"><</a>
</li>
</template>
<template v-for="(n,index) in pageList" :key=index>
<template v-if="page==n">
<li class="page-item">
<span class="page-link" :key="index">{{ n }}</span>
</li>
</template>
<template v-else>
<li class="page-item">
<a class="page-link" href="javascript:;" @click="fnPage(`${n}`)" :key="index">{{ n }}</a>
</li>
</template>
</template>
<template v-if="next">
<li class="page-item">
<a class="page-link" href="javascript:;" @click="fnPage(`${end+1}`)">></a>
<a class="page-link" href="javascript:;" @click="fnPage(`${totalPage}`)">>></a>
</li>
</template>
</ul>
</nav>
..
<script>
import axios from "axios";
export default {
data() { // 동적 바인딩 data
return {
requestBody: {}, // 리스트 페이지 데이터전송
list: null,
type: "",
keyword: "",
// page
totalCount: 0,
prev: false,
next: false,
page: 1,
pageList: [],
size: 10,
start: 1,
end: 0,
totalPage: 0,
}
},
mounted() {
this.fnGetList();
},
methods: {
detail(id) {
this.$router.push({
path: `/board/detail/${id}`,
query: this.requestBody
});
},
fnGetList() {
this.requestBody = { // 데이터 전송
type: this.type,
keyword: this.keyword,
page: this.page,
size: this.size
}
axios.get("/api/board/list-page", {
params: this.requestBody
}).then((res) => {
console.log(res);
this.list = res.data.content;
// page
this.totalCount = res.data.totalCount;
this.prev = res.data.prev;
this.next = res.data.next;
this.page = res.data.page;
this.pageList = res.data.pageList;
this.size = res.data.size;
this.start = res.data.start;
this.end = res.data.end;
this.totalPage = res.data.totalPage;
}).catch((err) => {
console.log(err);
})
},
fnPage(n) {
if (this.page !== n) {
this.page = n;
}
this.fnGetList()
},
},
}
</script>
소스출처