본문 바로가기

프로젝트/booktalk(책 중고 거래 서비스)

product

내가한 부분은 아니지만 코드를 보면서 공부해보기로 했다.

 

productController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v2/products")
public class ProductController {

    private final ProductService productService;

    // 상품 생성 엔드포인트
    @PostMapping
    public ResponseEntity<ProductCreateRes> createProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestPart("req") ProductCreateReq req,
        @RequestParam("upload") List<MultipartFile> files
    ) throws IOException {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.createProduct(userDetails.getUser().getId(), req, files));
    }

    // 상품 업데이트 엔드포인트
    @PutMapping("/{productId}")
    public ResponseEntity<ProductUpdateRes> updateProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable Long productId,
        @RequestPart("req") ProductUpdateReq req,
        @RequestParam("upload") List<MultipartFile> files
    ) throws IOException {
        return ResponseEntity.status(HttpStatus.OK)
            .body(
                productService.updateProduct(userDetails.getUser().getId(), productId, req, files));
    }

    // 상품 삭제 엔드포인트
    @DeleteMapping("/{productId}")
    public ResponseEntity<ProductDeleteRes> deleteProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable Long productId
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.deleteProduct(userDetails.getUser().getId(), productId));
    }

    // 특정 상품 조회 엔드포인트
    @GetMapping("/{productId}")
    public ResponseEntity<ProductGetRes> getProduct(
        @PathVariable Long productId
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProduct(productId));
    }

    // 전체 상품 리스트 조회 엔드포인트
    @GetMapping
    public ResponseEntity<Page<ProductListRes>> getProductList(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductList(page - 1, size, sortBy, isAsc));
    }

    // 메인화면에 출력할 관심상품 Top3 조회 엔드포인트
    @GetMapping("/main")
    public ResponseEntity<List<ProductTopLikesListRes>> getProductListTopThree() {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductListByLikesTopThree());
    }

    // 상품 검색 리스트 조회 엔드포인트
    @GetMapping("/search")
    public ResponseEntity<Page<ProductSerachListRes>> getProductSearchList(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc,
        @RequestParam(value = "query") String search
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductSearchList(page - 1, size, sortBy, isAsc, search));
    }

    // 태그에 따른 상품 리스트 조회 엔드포인트
    @GetMapping("/tag")
    public ResponseEntity<Page<ProductTagListRes>> getProductListByTag(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc,
        @RequestParam(value = "tag") String search
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductSearchTagList(page - 1, size, sortBy, isAsc, search));
    }
}

 

 

productEntity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "TB_PRODUCT")
public class Product extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 상품의 고유 식별자

    @Column(nullable = false)
    private String name; // 상품 이름

    @Column(nullable = false)
    private Long price; // 상품 가격

    @Column(nullable = false)
    private Long quantity; // 상품 수량

    @Column(nullable = false)
    private Long productLikeCnt; // 상품의 좋아요 개수

    @Enumerated(EnumType.STRING)
    private Region region; // 상품이 속한 지역

    @Column(nullable = false)
    private Boolean finished; // 거래 상태가 완료되었는지 여부

    @Column(nullable = false)
    private String content; // 상품 설명 내용

    private Boolean deleted; // 상품이 삭제되었는지 여부

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user; // 상품을 등록한 사용자

    @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private final List<ProductCategory> productCategoryList = new ArrayList<>(); // 상품에 속한 카테고리 리스트

    @Builder
    private Product(String name, Long price, Long quantity, Region region, String content,
        User user) {
        this.productLikeCnt = 0L;
        this.name = name;
        this.quantity = quantity;
        this.price = price;
        this.region = region;
        this.content = content;
        this.user = user;
        this.finished = false;
        this.deleted = false;
    }

    // 업데이트 메서드
    public void update(ProductUpdateReq req) {
        this.name = req.name();
        this.quantity = req.quantity();
        this.price = req.price();
        this.region = req.region();
        this.content = req.content();
        this.finished = req.finished();
    }

    // 거래 상태를 완료로 변경하는 메서드
    public void finish() {
        this.finished = true;
    }

    // 상품을 삭제 상태로 변경하는 메서드
    public void deleted() {
        this.deleted = true;
    }

    // 상품에 속한 카테고리 추가 메서드
    public void addProductCategory(ProductCategory productCategory) {
        this.productCategoryList.add(productCategory);
        productCategory.setProduct(this);
    }

    // 상품 좋아요 개수 업데이트 메서드
    public void updateProductLikeCnt(Boolean updated) {
        if (updated) {
            this.productLikeCnt++;
        } else {
            this.productLikeCnt--;
        }
    }
}

 

 

enum Region

@Getter
public enum Region {

    SEOUL("서울"),
    BUSAN("부산"),
    INCHEON("인천"),
    DAEGU("대구"),
    GWANGJU("광주"),
    DAEJEON("대전"),
    ULSAN("울산"),
    SUWON("수원"),
    CHEONGJU("청주"),
    JEJU("제주");

    private final String name;

    // 생성자
    Region(String name) {
        this.name = name;
    }

    // 지역 이름 반환
    public String getName() {
        return name;
    }

}

 

 

ProductRepository

/**
 * 상품 정보에 대한 데이터 액세스를 담당하는 JpaRepository 인터페이스입니다.
 * Custom 쿼리 메서드 및 페이징 처리, 특정 예외 처리 등이 정의되어 있습니다.
 */
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {

    /**
     * 삭제되지 않은 모든 상품을 페이지별로 조회하는 메서드입니다.
     *
     * @param pageable 페이징 정보
     * @return 페이징 처리된 상품 목록
     */
    Page<Product> findAllByDeletedFalse(Pageable pageable);

    /**
     * 좋아요 개수가 높은 순으로 상위 3개의 상품을 조회하는 메서드입니다.
     *
     * @return 좋아요 개수가 높은 상위 3개의 상품 목록
     */
    List<Product> findTop3ByDeletedFalseOrderByProductLikeCntDesc();

    /**
     * 특정 사용자가 등록한 상품을 조회하는 메서드입니다.
     *
     * @param userId 사용자 ID
     * @return 특정 사용자가 등록한 상품 목록
     */
    List<Product> findProductsByUserId(Long userId);

    /**
     * 상품 ID로 상품을 조회하고, 존재하지 않을 경우 NotFoundProductException을 throw하는 메서드입니다.
     *
     * @param id 조회할 상품의 ID
     * @return 조회된 상품 엔티티
     * @throws NotFoundProductException 상품이 존재하지 않을 경우 발생하는 예외
     */
    default Product findProductByIdWithThrow(Long id) {
        return findById(id).orElseThrow(() ->
            new NotFoundProductException(ProductErrorCode.NOT_FOUND_PRODUCT));
    }
}

 

ProductRepositoryCustom

/**
 * 상품과 관련된 커스텀 쿼리 메서드를 정의하는 인터페이스입니다.
 * JpaRepository에서 제공하는 기본 메서드 외에 추가적인 쿼리 메서드를 선언합니다.
 */
public interface ProductRepositoryCustom {

    /**
     * 상품 이름으로 검색하여 페이징된 상품 목록을 조회하는 메서드입니다.
     *
     * @param pageable 페이징 정보
     * @param search   검색어 (상품 이름)
     * @return 페이징된 상품 목록
     */
    Page<Product> getPostListByName(Pageable pageable, String search);

    /**
     * 특정 태그를 가진 상품을 페이징하여 조회하는 메서드입니다.
     *
     * @param pageable 페이징 정보
     * @param tag      검색할 태그
     * @return 페이징된 상품 목록
     */
    Page<Product> getProductListByTag(Pageable pageable, String tag);
}

 

 

ProductRepositoryCustomImpl

/**
 * 상품과 관련된 커스텀 쿼리 메서드를 구현하는 리포지토리 클래스입니다.
 */
@Repository
@RequiredArgsConstructor
public class ProductRepositoryCustomImpl implements ProductRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;
    QProduct product = QProduct.product;

    /**
     * 상품 이름으로 검색하여 페이징된 상품 목록을 조회하는 메서드입니다.
     *
     * @param pageable 페이징 정보
     * @param search   검색어 (상품 이름)
     * @return 페이징된 상품 목록
     */
    @Override
    public Page<Product> getPostListByName(Pageable pageable, String search) {

        JPAQuery<Product> query = jpaQueryFactory
            .selectFrom(product)
            .where(product.deleted.eq(false))
            .where(product.name.contains(search));

        // 정렬 적용
        if (pageable.getSort().isSorted()) {
            for (Sort.Order order : pageable.getSort()) {
                PathBuilder<Product> pathBuilder = new PathBuilder<>(Product.class,
                    product.getMetadata());
                query.orderBy(new OrderSpecifier<>(order.isAscending() ? Order.ASC : Order.DESC,
                    pathBuilder.get(order.getProperty(), Comparable.class)));
            }
        }

        List<Product> productList = query
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        return PageableExecutionUtils.getPage(productList, pageable,
            () -> query.fetchCount());
    }

    /**
     * 특정 태그를 가진 상품을 페이징하여 조회하는 메서드입니다.
     *
     * @param pageable 페이징 정보
     * @param tag      검색할 태그
     * @return 페이징된 상품 목록
     */
    @Override
    public Page<Product> getProductListByTag(Pageable pageable, String tag) {

        QProductCategory productCategory = QProductCategory.productCategory;

        JPAQuery<Product> query = jpaQueryFactory
            .selectFrom(product)
            .leftJoin(product.productCategoryList, productCategory).fetchJoin()
            .where(product.deleted.eq(false))
            .where(hasTag(tag));

        // 정렬 적용
        if (pageable.getSort().isSorted()) {
            for (Sort.Order order : pageable.getSort()) {
                PathBuilder<Product> pathBuilder = new PathBuilder<>(Product.class,
                    product.getMetadata());
                query.orderBy(new OrderSpecifier<>(order.isAscending() ? Order.ASC : Order.DESC,
                    pathBuilder.get(order.getProperty(), Comparable.class)));
            }
        }

        List<Product> productList = query
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        // distinct() !
        return PageableExecutionUtils.getPage(productList, pageable,
            () -> query.distinct().fetchCount());
    }

    /**
     * 특정 태그를 가진 상품을 검색하는데 필요한 BooleanExpression을 생성하는 메서드입니다.
     *
     * @param tagName 검색할 태그 이름
     * @return BooleanExpression
     */
    private BooleanExpression hasTag(String tagName) {
        return product.productCategoryList.any()
            .category.name.eq(tagName);
    }
}

 

productService

@Service
@RequiredArgsConstructor
@Transactional
public class ProductService {

    private final ProductRepository productRepository;
    private final UserRepository userRepository;
    private final CategoryRepository categoryRepository;
    private final ProductCategoryRepository productCategoryRepository;
    private final ImageFileService imageFileService;

    // Constructor-based injection is used here.

    // Method to create a new product
    public ProductCreateRes createProduct(Long userId, ProductCreateReq req, List<MultipartFile> files) throws IOException {
        // Find the user by ID
        User user = userRepository.findUserByIdWithThrow(userId);

        // Create a new Product entity
        Product product = Product.builder()
            .name(req.name())
            .price(req.price())
            .quantity(req.quantity())
            .region(req.region())
            .content(req.content())
            .user(user)
            .build();

        // Save the product in the repository
        product = productRepository.save(product);

        // Add categories to the product
        addCategory(req.categoryList(), product);

        // Create a UserRes object
        UserRes userRes = new UserRes(user.getId(), user.getNickname());

        // Create images for the product
        List<ImageCreateRes> imageCreateResList = imageFileService.createImage(userId, product.getId(), files);

        // Return a response with the created product information
        return new ProductCreateRes(product.getId(), product.getName(), product.getQuantity(),
            product.getPrice(), product.getRegion(), product.getFinished(), userRes,
            product.getContent(), req.categoryList(), imageCreateResList);
    }

    // Method to update an existing product
    public ProductUpdateRes updateProduct(Long userId, Long productId, ProductUpdateReq req, List<MultipartFile> files) throws IOException {
        // Find the user by ID
        User user = userRepository.findUserByIdWithThrow(userId);
        
        // Find the product by ID
        Product product = productRepository.findProductByIdWithThrow(productId);
        
        // Validate that the user has permission to update the product
        validateProductUser(user, product);

        // Update images for the product
        List<ImageCreateRes> imageCreateResList = imageFileService.updateImage(userId, productId, files);

        // Update the product information
        product.update(req);
        updateCategory(req.categoryList(), product);

        // Create a UserRes object
        UserRes userRes = new UserRes(user.getId(), user.getNickname());

        // Return a response with the updated product information
        return new ProductUpdateRes(product.getId(), product.getName(), product.getQuantity(),
            product.getPrice(), product.getRegion(), product.getFinished(), userRes,
            product.getProductLikeCnt(), product.getContent(), req.categoryList(),
            imageCreateResList);
    }

    // Method to get information about a specific product
    @Transactional(readOnly = true)
    public ProductGetRes getProduct(Long productId) {
        // Find the product by ID
        Product product = productRepository.findProductByIdWithThrow(productId);
        
        // Get the user associated with the product
        User user = product.getUser();
        
        // Create a UserRes object
        UserRes userRes = new UserRes(user.getId(), user.getNickname());

        // Get categories associated with the product
        List<String> categories = product.getProductCategoryList().stream()
            .map(productCategory -> productCategory.getCategory().getName())
            .toList();

        // Get images associated with the product
        List<ImageListRes> imageListRes =