내가한 부분은 아니지만 코드를 보면서 공부해보기로 했다.
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 =
'프로젝트 > booktalk(책 중고 거래 서비스)' 카테고리의 다른 글
jwt 토큰 인증 인가 (0) | 2024.02.02 |
---|---|
QueryDSL product 조회 코드 구현(페이징 처리,카테고리처리, 조회 결과 정렬) (0) | 2024.01.30 |
image파일 업로드 할때 리사이징 적용하기 (0) | 2024.01.24 |
[SpringBoot] AWS S3로 이미지 업로드하기 (1) | 2024.01.23 |
1/15 (0) | 2024.01.23 |