Spring의 Page 구현체
/*
* Copyright 2008-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.domain;
import java.util.List;
import java.util.function.Function;
import org.springframework.lang.Nullable;
/**
* Basic {@code Page} implementation.
*
* @param <T> the type of which the page consists.
* @author Oliver Gierke
* @author Mark Paluch
*/
public class PageImpl<T> extends Chunk<T> implements Page<T> {
private static final long serialVersionUID = 867755909294344406L;
private final long total;
/**
* Constructor of {@code PageImpl}.
*
* @param content the content of this page, must not be {@literal null}.
* @param pageable the paging information, must not be {@literal null}.
* @param total the total amount of items available. The total might be adapted considering the length of the content
* given, if it is going to be the content of the last page. This is in place to mitigate inconsistencies.
*/
public PageImpl(List<T> content, Pageable pageable, long total) {
super(content, pageable);
this.total = pageable.toOptional().filter(it -> !content.isEmpty())//
.filter(it -> it.getOffset() + it.getPageSize() > total)//
.map(it -> it.getOffset() + content.size())//
.orElse(total);
}
/**
* Creates a new {@link PageImpl} with the given content. This will result in the created {@link Page} being identical
* to the entire {@link List}.
*
* @param content must not be {@literal null}.
*/
public PageImpl(List<T> content) {
this(content, Pageable.unpaged(), null == content ? 0 : content.size());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.Page#getTotalPages()
*/
@Override
public int getTotalPages() {
return getSize() == 0 ? 1 : (int) Math.ceil((double) total / (double) getSize());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.Page#getTotalElements()
*/
@Override
public long getTotalElements() {
return total;
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.Slice#hasNext()
*/
@Override
public boolean hasNext() {
return getNumber() + 1 < getTotalPages();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.Slice#isLast()
*/
@Override
public boolean isLast() {
return !hasNext();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.Slice#transform(org.springframework.core.convert.converter.Converter)
*/
@Override
public <U> Page<U> map(Function<? super T, ? extends U> converter) {
return new PageImpl<>(getConvertedContent(converter), getPageable(), total);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String contentType = "UNKNOWN";
List<T> content = getContent();
if (!content.isEmpty() && content.get(0) != null) {
contentType = content.get(0).getClass().getName();
}
return String.format("Page %s of %d containing %s instances", getNumber() + 1, getTotalPages(), contentType);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PageImpl<?>)) {
return false;
}
PageImpl<?> that = (PageImpl<?>) obj;
return this.total == that.total && super.equals(obj);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += 31 * (int) (total ^ total >>> 32);
result += 31 * super.hashCode();
return result;
}
}
Client → Server
totalPages = totalElement / size 결과를 소수점 올림
1 / 10 = 0.1 => 총 1 페이지
9 / 10 = 0.9 => 총 1페이지
10 / 10 = 1 => 총 1페이지
11 / 10 => 1.1 => 총 2페이지
Page<Product> --> Dto로 변환해서 반환해도 되지만, 페이지 객체 그대로 반환할 수도 있음
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.service.ProductService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor //의존성 주입
public class ProductController {
//HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
private final ProductService productService;
//관심상품 추가 할 때 토큰 보내기
// 관심 상품 등록하기
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, HttpServletRequest request) {
// 응답 보내기
return productService.createProduct(requestDto, request);
}
//관심상품 조회 할 때 토큰 보내기
// 관심 상품 조회하기
@GetMapping("/products")
public Page<Product> getProducts(
@RequestParam("page") int page, //쿼리 방식으로 데이터를 가져옴
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
HttpServletRequest request
) {
// 응답 보내기
//page-1: 페이지 객체에서는 0번 인덱스가 1 페이지
return productService.getProducts(request, page-1, size, sortBy, isAsc);
}
//관심상품 최저가 추가 할 때 토큰 보내기
// 관심 상품 최저가 등록하기
@PutMapping("/products/{id}")
public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto, HttpServletRequest request) {
// 응답 보내기 (업데이트된 상품 id)
//request: 토큰을 가져와야하므로 넣었음
return productService.updateProduct(id, requestDto, request);
}
}
package com.sparta.myselectshop.service;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final UserRepository userRepository; //의존성 주입
private final JwtUtil jwtUtil; //의존성 주입
//관심상품 추가 할 때 토큰 보내기
//등록하기
@Transactional
public ProductResponseDto createProduct(ProductRequestDto requestDto, HttpServletRequest request) {
// Request에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
//JWT 안에 있는 정보를 담는 Claims 객체
Claims claims;
// 토큰이 있는 경우에만 관심상품 추가 가능
if (token != null) {
//validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
if (jwtUtil.validateToken(token)) {
//true 라면, 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
//false 라면,
} else {
//해당 메시지 반환
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
//claims.getSubject(): 우리가 넣어두었던 username 가져오기
//findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
// 요청받은 DTO 로 DB에 저장할 객체 만들기
Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));
return new ProductResponseDto(product);
//토큰이 null 이라면(Client 에게서 Token 이 넘어오지 않은 경우),
} else {
//null 을 반환
return null;
}
}
//관심상품 조회 할 때 토큰 보내기
//조회하기 (Token 을 통해 검증한 다음, 검증된 사람만 조회 가능)
@Transactional(readOnly = true)
//page: page-1 한 숫자
public Page<Product> getProducts(HttpServletRequest request, int page, int size, String sortBy, boolean isAsc) {
// 페이징 처리
//true 면 오름차순, false 면 내림자순
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
//direction: 오름차순/내림차순
//sortBy: 어떤 것을 기준으로 오름차순/내림차순 판단할 것인지
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort); //Pageable --> 구글링
// Request에서 Token 가져오기
String token = jwtUtil.resolveToken(request); //의존성 주입 필요
Claims claims;
// 토큰이 있는 경우에만 관심상품 조회 가능
if (token != null) {
// Token 검증
if (jwtUtil.validateToken(token)) {
// 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
} else {
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
//사용자 권한 가져와서,
//ADMIN 이면 전체 조회
//USER 면 본인이 추가한 부분만 조회
//user: 위의 User 객체에서 가져온 것
//userRoleEnum: 가져온 권한을 담는 곳
UserRoleEnum userRoleEnum = user.getRole();
System.out.println("role = " + userRoleEnum);
Page<Product> products;
//사용자 권한이 USER 일 경우
if (userRoleEnum == UserRoleEnum.USER) {
//UserId 가 동일한 product 를 가져와서 products 에 담는다
//id 와 pageable 을 통해 products 를 찾음
products = productRepository.findAllByUserId(user.getId(), pageable);
//사용자 권한이 ADMIN 일 경우
} else {
//상관없이 모든걸 다 가져온다(findAll())
//pageable: 페이징 처리가 필요하므로 넣음
products = productRepository.findAll(pageable);
}
//반환
return products;
} else {
return null;
}
}
//관심상품 최저가 추가 할 때 토큰 보내기
@Transactional
//Long id: product 의 id --> update 하기 위해서는 먼저 어떤 product 인지 확인하고, 그 product 의 myprice 를 update
public Long updateProduct(Long id, ProductMypriceRequestDto requestDto, HttpServletRequest request) {
// Request에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
Claims claims;
// 토큰이 있는 경우에만 관심상품 최저가 업데이트 가능
if (token != null) {
// Token 검증
if (jwtUtil.validateToken(token)) {
// 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
} else {
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
//product 안에 userid 가 추가돼서, productid(id) 와 userid(user.getId()) 둘 다 필요
//내가 가지고 온 productid 이면서, 그 product 가 동일한 userid 를 가지고 있는지까지 확인
//즉, 현재 로그인한 user 가 선택한 product 가 맞는지 확인
Product product = productRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
() -> new NullPointerException("해당 상품은 존재하지 않습니다.")
);
//동일하다면, update() 메서드 사용
product.update(requestDto);
//product 의 id 반환
return product.getId();
} else {
return null;
}
}
@Transactional //설정해둔 myprice 값 보다 수정된 lprice 값이 작다면, '최저가' 표시가 뜨도록 js 에서 설정되어 있음
public void updateBySearch (Long id, ItemDto itemDto){
//가지고 온 id 로 product 가 있는지 없는지 확인부터 한다
Product product = productRepository.findById(id).orElseThrow(
() -> new NullPointerException("해당 상품은 존재하지 않습니다.")
);
//그리고 itemDto 를 넣어서 update 를 실시
product.updateByItemDto(itemDto);
}
}
package com.sparta.myselectshop.repository;
import com.sparta.myselectshop.entity.Product;
import org.springframework.data.domain.Page; //패키지도 확인하도록 주의
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
//UserId 를 통해, userId 가 동일한 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
//Page 객체로 받는다
Page<Product> findAllByUserId(Long userId, Pageable pageable);
//Product 의 id 와 userId 가 일치하는 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
Optional<Product> findByIdAndUserId(Long id, Long userId);
Page<Product> findAll(Pageable pageable);
}