Project MySelectShop - JPA (1)

박영준·2022년 12월 5일
0

Java

목록 보기
22/111

MySelectShop

필요한 기능 확인하기

  1. 키워드로 상품 검색하고 그 결과를 목록으로 보여주기
  2. 회원가입
  3. 로그인
  4. 로그인 성공 시 토큰 발급
  5. 로그아웃
  6. 로그인 한 유저만 관심상품 등록, 조회, 최저가 등록 가능
  7. ADMIN 계정은 모든 상품 조회 가능
  8. 관심상품 목록 페이징 및 정렬 가능
  9. 폴더 생성 및 조회 가능
  10. 관심상품에 폴더 추가 기능
  11. 폴더 별 관심상품 조회 가능

Product API

User API

페이징 및 정렬 설계

페이징 및 정렬 설계

  • 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

  1. 페이징
    1. page : 조회할 페이지 번호 (1부터 시작)
    2. size : 한 페이지에 보여줄 상품 개수 (10개로 고정!)
  2. 정렬
    1. sortBy (정렬 항목)
      1. id : Product 테이블의 id
      2. title : 상품명
      3. lprice : 최저가
    2. isAsc (오름차순?)
      1. true: 오름차순 (asc)
      2. false : 내림차순 (desc)
  • Server → Client
    - number: 조회된 페이지 번호 (0부터 시작)
    - content: 조회된 상품 정보 (배열)
    - size: 한 페이지에 보여줄 상품 개수
    - first: 첫 페이지인지? (boolean)
    - last: 마지막 페이지인지? (boolean)
    - numberOfElements: 실제 조회된 상품 개수
    - totalElements: 전체 상품 개수 (회원이 등록한 모든 상품의 개수)
    - totalPages: 전체 페이지 수
    totalPages = totalElement / size 결과를 소수점 올림
    1 / 10 = 0.1 =>1 페이지
    9 / 10 = 0.9 =>1페이지
    10 / 10 = 1 =>1페이지
    11 / 10 => 1.1 =>2페이지

Product API

Page<Product> --> Dto로 변환해서 반환해도 되지만, 페이지 객체 그대로 반환할 수도 있음

페이징 및 정렬 구현

1. ProductController

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);
    }

}

2. ProductService

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);
    }
}

3. ProductRepository

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);
}
profile
개발자로 거듭나기!

0개의 댓글