Java/Spring

스프링부트 프로젝트 최종 결과물

SeungbeomKim 2022. 8. 22. 01:13

ShoppingMall을 구현하기 위해 사용된 package

Review

package com.studyProjectA.ShoppingMall.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.minidev.json.annotate.JsonIgnore;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.time.LocalDate;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Review {

    // 아이디
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 상품 매핑
    @JoinColumn(name = "Product_id")
    @ManyToOne(fetch = FetchType.LAZY)
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JsonIgnore
    private Product product;

    // 평점
    @Column(nullable = false)
    private Integer rate;

    // 리뷰 코멘트
    @Column(nullable = false)
    private String comment;

    // 리뷰생성날짜
    @Column(nullable = false)
    @CreatedDate
    @DateTimeFormat(pattern = "yyyy-mm-dd")
    private LocalDate createDate;

    @PrePersist
    public void createDate() {
        this.createDate = LocalDate.now();
    }

    // 유저 매핑
    @JoinColumn(name = "User_id")
    @ManyToOne(fetch = FetchType.LAZY)
    @OnDelete(action = OnDeleteAction.NO_ACTION)
    @JsonIgnore
    private User user;

    @Builder
    public Review(Product product, Integer rate, LocalDate createDate, String comment, User user)
    {
        this.product = product;
        this.rate = rate;
        this.createDate = createDate;
        this.comment = comment;
        this.user = user;
    }

}
  • @JsonIgnore : Json으로 해당 데이터가 Return 될때, Response에 해당 필드가 제외된다. 
  • @Builder 어노테이션 사용이유(생성자에서 인자가 많을 경우 사용)
  • 생성자 파라미터가 많을 경우 가독성이 좋지 않다.
  • 필요 데이터만 설정 가능
  • 유연성 및 가독성 확보
  • 객체의 일관성을 유지할 수 있다.
  • 코드의 간소화
  • 변경 가능성을 최소화 할 수 있다.

ReviewController

package com.studyProjectA.ShoppingMall.controller;

import com.studyProjectA.ShoppingMall.dto.ReviewRequestDto;
import com.studyProjectA.ShoppingMall.dto.ReviewResponseDto;
import com.studyProjectA.ShoppingMall.dto.UserDto;
import com.studyProjectA.ShoppingMall.entity.Review;
import com.studyProjectA.ShoppingMall.entity.User;
import com.studyProjectA.ShoppingMall.excpetion.ReviewNotFoundException;
import com.studyProjectA.ShoppingMall.excpetion.UserNotEqualsException;
import com.studyProjectA.ShoppingMall.excpetion.UserNotFoundException;
import com.studyProjectA.ShoppingMall.repository.ReviewRepository;
import com.studyProjectA.ShoppingMall.repository.UserRepository;
import com.studyProjectA.ShoppingMall.response.Response;
import com.studyProjectA.ShoppingMall.service.ProductService;
import com.studyProjectA.ShoppingMall.service.ReviewService;
import com.studyProjectA.ShoppingMall.service.UserService;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

import static com.studyProjectA.ShoppingMall.response.Response.*;

/*
Client와 가장 맞붙어있으며, 데이터를 받아오고 JSON파일로 넘겨주는 역할 담당

Controller의 역할
- 리뷰 모두 불러오기
- 리뷰 저장하기
- 리뷰 삭제하기
- 리뷰 수정하기
 */

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class ReviewController {

    private final ReviewService reviewService;
    private final UserRepository userRepository;



    @ApiOperation(value = "특정 제품의 전체 리뷰 게시글 보기", notes = "전체 리뷰 게시글을 조회합니다.")
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/products/{productId}/reviews/")
    public Response findAll(@PathVariable("productId") Long productId) {
        return success(reviewService.getProductReviews(productId));
    }
    //@Pathvariable을 사용한 이유는 리뷰 전체조회지만, 몇 번 제품의 전체 리뷰조회인지 확인하기
    //위해 해당 경로변수에 대한 백엔드의 서버요청이 필요하기 때문에 사용했다.

    @ApiOperation(value = "특정 제품의 단건 리뷰 보기", notes = "리뷰 일부를 조회합니다.")
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/products/{productId}/reviews/{reviewsId}")
    public Response findReview(@PathVariable("productId") Long productId, @PathVariable("reviewId") Long reviewId) {
        return success(reviewService.getProductReview(reviewId));
    }

    @ApiOperation(value = "리뷰 게시글 작성", notes = "리뷰 게시글을 작성합니다.")
    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping("/products/{productId}/reviews/write")
    public Response writeReview(@RequestBody @Valid ReviewRequestDto reviewRequestDto, @PathVariable("productId") Long productId)
    {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User loginUser = userRepository.findByUsername(authentication.getName()).orElseThrow(UserNotFoundException::new);

        return success(reviewService.saveReview(reviewRequestDto, loginUser, productId));

    }

    @ApiOperation(value = "리뷰 게시글 수정", notes = "리뷰 게시글을 수정합니다.")
    @ResponseStatus(HttpStatus.OK)
    @PutMapping("/products/{productId}/reviews/update/{reviewId}")
    public Response updateReview(@PathVariable("productId") Long productId, @PathVariable("reviewId")Long reviewId, @RequestBody @Valid ReviewRequestDto reviewRequestDto) {



        //게시글에 대한 정보 및 게시글 작성자를 꺼낸다
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User loginUser = userRepository.findByUsername(authentication.getName()).orElseThrow(UserNotFoundException::new);

        return success(reviewService.updateReview(reviewRequestDto,loginUser,reviewId));



    }

    @ApiOperation(value = "리뷰 게시글 삭제", notes = "리뷰 게시글을 삭제합니다.")
    @ResponseStatus(HttpStatus.OK)
    @DeleteMapping("/products/{productId}/reviews/delete/{reviewId}")
    public Response deleteReview(@PathVariable("reviewId") Long reviewId) {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User loginUser = userRepository.findByUsername(authentication.getName()).orElseThrow(UserNotFoundException::new);
        reviewService.deleteReview(reviewId,loginUser);
        return success("삭제 완료");
    }
    @ApiOperation(value = "리뷰 작성자로 검색",notes = "해당 유저의 제품 리뷰를 검색힙니다.")
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/products/{productId}/reviews/byUser/{username}")
    public Response findReviewByUsername(@PathVariable("productId") Long productId, @PathVariable("username") String username){
        return success(reviewService.getProductReviewsByUsername(productId, username));
    }

    @ApiOperation(value = "리뷰 내용으로 검색", notes = "리뷰에 등록된 내용을 검색합니다. ")
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/products/{productId}/reviews/byComment/{comment}")
    public Response findReviewByComment(@PathVariable("productId")Long productId, @PathVariable("comment")String comment){
        return success(reviewService.getProductReviewsByComment(productId, comment));
    }

}

 

@RequestMapping(요청 매핑) : 클라이언트에서 요청하는 url를 controller에 매핑시켜주는 역할

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User loginUser = userRepository.findByUsername(authentication.getName()).orElseThrow(UserNotFoundException::new);

다음 코드부분은 토큰(유저의 정보)이 담긴 부분이다.

서버가 요청 보낸 사용자의 토큰을 바탕으로 요청 보낸 사용자의 토큰을 꺼낸다.

꺼내온 토큰이 요청 보낸 사용자의 것이 맞는 확인해준다.(맞지 않으면 사용자 예외처리 적용)

 

ReviewService

@Service
@RequiredArgsConstructor
public class ReviewService {

    private final ReviewRepository reviewRepository;
    private final ProductRepository productRepository;

    private final UserRepository userRepository;
    // Read All
    @Transactional(readOnly = true)
    public List<ReviewResponseDto> getProductReviews(Long productId) {
        Product product = productRepository.findById(productId).orElseThrow(ProductNotFoundException::new);
        //해당 번호의 제품이 없으면 예외처리
        List<Review> reviews = reviewRepository.findAllByProduct_Id(productId).orElseThrow(ReviewNotFoundException::new);
        //해당 번호의 제품의 리뷰가 존재하지 않으면 예외처리
        List <ReviewResponseDto> reviewResponseDtos = new ArrayList<>();
        //기존 리뷰에 있는 내용들을 담기위해 빈 배열 생성
        for(Review review : reviews){
            reviewResponseDtos.add(ReviewResponseDto.toDto(review));
        }
        //기존 리뷰 내용들을 전부 ResponseDto에 담아준다.
        return reviewResponseDtos;
    }

    @Transactional(readOnly = true)
    public ReviewResponseDto getProductReview(Long reviewId)
    {


        Review review = reviewRepository.findById(reviewId).orElseThrow(ReviewNotFoundException::new);
        //리뷰 존재하지 않으면 예외처리

            ReviewResponseDto reviewResponseDto = ReviewResponseDto.toDto(review);
        //리뷰 반환
        return reviewResponseDto;
    }
    // Create
    @Transactional
    public Review saveReview(ReviewRequestDto reviewRequestDto, User writer, Long productId) {
        Product product = productRepository.findById(productId).orElseThrow(ProductNotFoundException::new);
        Review review = Review.builder()
                .comment(reviewRequestDto.getComment())
                .rate(reviewRequestDto.getRate())
                .product(product)
                .user(writer)
                .build();
        return reviewRepository.save(review);

    }

    // Update
    @Transactional
    public Review updateReview(ReviewRequestDto reviewRequestDto, User writer, Long reviewId) {
        Review oldReview = reviewRepository.findById(reviewId).orElseThrow(ReviewNotFoundException::new);
        User loginUser = oldReview.getUser();
        if(writer.equals(loginUser)){
            oldReview.setRate(reviewRequestDto.getRate());
            oldReview.setComment(reviewRequestDto.getComment());
            return  reviewRepository.save(oldReview);
        }
        else{
            throw new UserNotEqualsException();
            //게시글 작성자와 토큰 보낸 사람의 정보가 불일치할때의 예외처리
        }

    }

    // Delete
    @Transactional
    public void deleteReview(Long reviewId,User writer) {

        Review oldReview = reviewRepository.findById(reviewId).orElseThrow(ReviewNotFoundException::new);
        User loginUser = oldReview.getUser();
        if(writer.equals(loginUser)){
            reviewRepository.deleteById(reviewId);
        }
        else{
            throw new UserNotEqualsException();
            //게시글 작성자와 토큰 보낸 사람의 정보가 불일치할때의 예외처리
        }
    }
    @Transactional(readOnly = true)
    public List<ReviewResponseDto> getProductReviewsByUsername(Long productId, String username){
        Product product = productRepository.findById(productId).orElseThrow(ProductNotFoundException::new);
        User user = userRepository.findByUsername(username).orElseThrow(UserNotEqualsException::new);
        List<Review> reviews = reviewRepository.findAll();
        List<ReviewResponseDto> reviewResponseDtos = new ArrayList<>();
        for(Review review : reviews){
            if(review.getProduct().equals(product) && review.getUser().equals(user)){
                reviewResponseDtos.add(ReviewResponseDto.toDto(review));
            }
        }
        return reviewResponseDtos;
    }

    @Transactional(readOnly = true)
    public List<ReviewResponseDto> getProductReviewsByComment(Long productId, String comment){
        Product product = productRepository.findById(productId).orElseThrow(ProductNotFoundException::new);
        List<Review> reviews = reviewRepository.findAll();
        List<ReviewResponseDto> reviewResponseDtos = new ArrayList<>();
        for(Review review : reviews){
            if(review.getProduct().equals(product) && review.getComment().contains(comment)){
                reviewResponseDtos.add(ReviewResponseDto.toDto(review));
            }
        }
        return reviewResponseDtos;
    }

}

리뷰를 조회하기 위해서는 Service에서 해야 할 일은 우선적으로 다음과 같이, 해당 번호의 제품을 가져와야 한다.

Product product = productRepository.findById(productId).orElseThrow(ProductNotFoundException::new);

 

리뷰를 수정 및 삭제하기 위해서는 토큰 보낸 사람의 정보와 리뷰 게시글의 작성자의 정보가 같은지 확인하는 과정이 필요하다.

Review oldReview = reviewRepository.findById(reviewId).orElseThrow(ReviewNotFoundException::new);
        User loginUser = oldReview.getUser();
        if(writer.equals(loginUser)){
            oldReview.setRate(reviewRequestDto.getRate());
            oldReview.setComment(reviewRequestDto.getComment());
            return  reviewRepository.save(oldReview);
        }
        else{
            throw new UserNotEqualsException();
            //게시글 작성자와 토큰 보낸 사람의 정보가 불일치할때의 예외처리
        }

여기에서 writer는 토큰 보낸 사람의 정보가 되고 loginUser는 리뷰를 작성한 사람의 정보가 된다.

작성자의 토큰 = 서버에서 보낸 토큰 정보라면,

review를 수정 및 삭제하고, 그렇지 않다면 ㄱ예외처리 시켜주면 된다.

ReviewRepository

public interface ReviewRepository extends JpaRepository<Review, Long> {

    Optional<List<Review>> findAllByProduct_Id(Long id);


}

Repository에서 Optional 객체를 받은 이유는 의도치 않게 생기는 Null값으로 인한 예외처리를 손쉽게 처리하기 위해서 Optional 객체 타입을 사용하였다.

ReviewRequestDto

package com.studyProjectA.ShoppingMall.dto;

import com.studyProjectA.ShoppingMall.entity.Product;
import com.studyProjectA.ShoppingMall.entity.Review;
import com.studyProjectA.ShoppingMall.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;

/*
Controller에서 Service로 데이터를 넘겨줄 때
@NotBlank를 통해 데이터의 유효성 검사를 위한 Dto

@NotNull, @NotEmpty, @NotBlank 등은 Advice패키지에서 예외처리해준다.
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReviewRequestDto {


    @NotNull(message = "평점 입력")
    private Integer rate;

    @NotBlank(message = "리뷰 입력")
    private String comment;



}

RequestDto(요청 Dto)는 유효성 검사를 위해 만들어진 Dto이다. 또한 수정될 부분의 데이터들을 기존 감싸서 Entity로 보내기 위해 만든 Dto이다.

ReviewResponseDto

package com.studyProjectA.ShoppingMall.dto;

import com.studyProjectA.ShoppingMall.entity.Review;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;

/*
Client에게 Review 객체정보를 다 주지않고
원하는 데이터만 줄 수 있게 하는 Dto

user 이름
review 평점
product 판매자 회사이름 or 개인이름
review 작성날짜
product 상품 이름
review 리뷰멘트

이렇게만 페이지에 표시해주면 된다.
 */

@Data
public class ReviewResponseDto {

    @NotBlank
    private String buyerName;

    @NotNull
    private Integer rate;

    @NotBlank
    private String sellerName;

    @NotBlank
    private LocalDate date;

    @NotBlank
    private String productName;

    @NotBlank
    private String comment;

    // Constructor
    public ReviewResponseDto(String buyerName, int rate, String sellerName, LocalDate date, String productName, String comment) {
        this.buyerName = buyerName;
        this.rate = rate;
        this.sellerName = sellerName;
        this.date = date;
        this.productName = productName;
        this.comment = comment;
    }

    // toDto
    public static ReviewResponseDto toDto(Review review) {
        return new ReviewResponseDto(
                review.getUser().getUsername(),
                review.getRate(),
                review.getProduct().getSeller().getUsername(),
                review.getCreateDate(),
                review.getProduct().getProductName(),
                review.getComment()
        );
    }
}

Postman을 이용해 리뷰 기능 Test

전체 기능 

1.join(회원가입)

url주소를 입력해 post방식으로 다음과 같이 username, password, email, address를 json 값으로 전송하면 다음과 같이 데이터들이 저장된다. password는 암호화된 형태로 저장이 된다.

2.login(로그인)

로그인 부분은 username과 password만 존재하면 된다.

username과 password를 전송시키면,

Headers->Authorization에서 오른쪽 부분에 토큰이 생긴다.(이를 복사해서 모든 기능들에 토큰을 추가해줘야 한다)

3.myPage(마이페이지)

Mypage url에 복사한 토큰을 집어넣게 되면, 아까 회원가입때 입력했던 정보들이 보여진다.

4.write(리뷰 작성)

고객이 입력해야할 review(평점(rate),리뷰내용(comment))를 json형식으로 보내주면 

다음처럼 제품에 대한 id값, 리뷰에 대한 id값 등이 보여지고 이 값을 이용해 다른 기능들에 적용할 수 있다.