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
또는, @PageableDefault 어노테이션으로 페이징Request를 처리할 수 있다.
// 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";
}
service
@Transactional(readOnly = true)
@Override
public Page<PostResponse> listPage1(Pageable pageable) {
return postRepository.findAll(pageable)
.map(PostResponse::toDto);
}
PageRequestDTO
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
private int page = 1;
@Builder.Default
private int size = 10;
@Builder.Default
private String sort = "desc";
}
PageResponseDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponseDTO<E> {
private List<E> dtoList;
private List<Integer> pageNumList;
private PageRequestDTO pageRequestDTO;
private boolean prev, next;
private int totalCount, prevPage, nextPage, totalPage, current;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(List<E> dtoList, PageRequestDTO pageRequestDTO, long totalCount) {
this.dtoList = dtoList;
this.pageRequestDTO = pageRequestDTO;
this.totalCount = (int)totalCount;
int end = (int)(Math.ceil( pageRequestDTO.getPage() / 10.0 )) * 10;
int start = end - 9;
int last = (int)(Math.ceil((totalCount/(double)pageRequestDTO.getSize())));
end = end > last ? last: end;
this.prev = start > 1;
this.next = totalCount > end * pageRequestDTO.getSize();
this.pageNumList = IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());
if(prev) {
this.prevPage = start -1;
}
if(next) {
this.nextPage = end + 1;
}
this.totalPage = this.pageNumList.size();
this.current = pageRequestDTO.getPage();
}
}
Service
@Override
public PageResponseDTO<PostResponse> listPage2(PageRequestDTO requestDTO) {
Pageable pageable = PageRequest.of(
requestDTO.getPage() - 1, //페이지 시작 번호가 0부터 시작하므로
requestDTO.getSize(),
"asc".equals(requestDTO.getSort()) ? // 정렬 조건
Sort.by("id").ascending() : Sort.by("id").descending()
);
Page<Post> result = postRepository.findAll(pageable);
return PageResponseDTO.<PostResponse>withAll()
.dtoList(result.stream().map(PostResponse::toDto).toList())
.totalCount(result.getTotalElements())
.pageRequestDTO(requestDTO)
.build();
}
Controller
@GetMapping("/list/page2")
public Response<PageResponseDTO<PostResponse>> listPage2(
PageRequestDTO requestDTO
){
return Response.success(postService.listPage2(requestDTO));
}
PageableRequestDto
public class PageableRequestDto extends PageRequestDto {
}
✅ 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>
소스출처