@Builder
@Schema(description = "판매자 상세 응답 정보")
public record SellerDetailResponse(
@Schema(description = "판매자 ID", example = "1")
Long sellerId,
@Schema(description = "이름", example = "홍길동")
String name,
@Schema(description = "가입 날짜", example = "2025-01-01T09:00:00")
LocalDateTime joinDate,
@Schema(description = "총 판매 중인 상품 수", example = "3")
int totalActiveProductCount,
@Schema(description = "총 판매 완료된 상품 수", example = "7")
int totalSoldProductCount
) {
public static SellerDetailResponse from(User seller, int totalActiveProductCount, int totalSoldProductCount) {
return SellerDetailResponse.builder()
.sellerId(seller.getId())
.name(seller.getName())
.joinDate(seller.getCreatedDate())
.totalActiveProductCount(totalActiveProductCount)
.totalSoldProductCount(totalSoldProductCount)
.build();
}
}
상품 목록들은 따로 쿼리를 통해 가져온다.
@Operation(summary = "판매자 기본 정보 조회", description = "판매자의 기본 정보와 통계를 조회합니다.")
@GetMapping("/{sellerId}/seller-info")
public ResponseEntity<ResponseDTO<UserDto.SellerDetailResponse>> getSellerInfo(
@Parameter(description = "조회할 사용자 ID", required = true) @PathVariable Long sellerId) {
UserDto.SellerDetailResponse response = userService.getSellerInfo(sellerId);
return ResponseEntity.ok(ResponseDTO.success(response));
}
@Operation(
summary = "판매자 상품 목록 조회",
description = "판매자의 상품 목록을 상태별로 조회합니다. 상태를 지정하지 않으면 모든 상품을 조회합니다." +
"예시 -> GET /api/users/123/products?status=ACTIVE&sort=createdDate,desc"
)
@GetMapping("/{sellerId}/products")
public ResponseEntity<ResponseDTO<Page<ProductDto.ProductDetailResponse>>> getSellerProducts(
@Parameter(description = "판매자 ID", required = true)
@PathVariable Long sellerId,
@Parameter(description = "상품 상태 (ACTIVE: 판매중, SOLD_OUT: 판매완료, 미지정시 전체)")
@RequestParam(required = false) ProductStatus status,
@Parameter(description = "페이지 정보 (기본: 10개씩, 최신순)")
@PageableDefault(size = 10, sort = "createdDate", direction = Sort.Direction.DESC)
Pageable pageable,
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {
Long currentUserId = userDetails != null ? userDetails.getUserId() : null;
Page<ProductDto.ProductDetailResponse> response = productQueryService.getProductsBySeller(
sellerId, status, pageable, currentUserId);
return ResponseEntity.ok(ResponseDTO.success(response));
}
판매자의 기본 정보를 가져오는 api와 판매자의 상품 목록을 가져오는 api를 분리하여 사용한다.
판매자의 상품 목록을 가져올 때는 상품 상태에 따라 가져올 수 있다.
// 판매자 기본 정보 조회
public UserDto.SellerDetailResponse getSellerInfo(Long sellerId) {
User seller = getUser(sellerId);
int activeCount = (int) productRepository.countBySellerIdAndStatus(sellerId, ProductStatus.ACTIVE);
int soldCount = (int) productRepository.countBySellerIdAndStatus(sellerId, ProductStatus.SOLD_OUT);
return UserDto.SellerDetailResponse.from(seller, activeCount, soldCount);
}
판매자의 프로필 정보를 조회하면서, 현재 판매 중인 상품 수와 판매 완료된 상품 수를 함께 반환한다.
/**
* 판매자의 특정 상태 상품을 이미지와 함께 조회 (Fetch Join 사용)
* N+1 문제 해결을 위해 이미지를 함께 로드
*/
@Query("select distinct p from Product p " +
"left join fetch p.images pi " +
"left join fetch p.seller " +
"where p.seller.id = :sellerId " +
"and p.status = :status")
Page<Product> findBySellerIdAndStatusWithImages(@Param("sellerId") Long sellerId,
@Param("status") ProductStatus status,
Pageable pageable);
// 판매자의 모든 상품을 이미지와 함께 조회 (상태 무관)
@Query("select distinct p from Product p " +
"left join fetch p.images pi " +
"left join fetch p.seller " +
"where p.seller.id = :sellerId")
Page<Product> findBySellerIdWithImages(@Param("sellerId") Long sellerId, Pageable pageable);
// 판매자의 전체 상품 수 조회 (상태 무관)
@Query("select count(p) from Product p where p.seller.id = :sellerId")
long countBySellerId(@Param("sellerId") Long sellerId);
findBySellerIdAndStatusWithImages
Product와 연관된 images, seller를 fetch join으로 한 번에 로드distinct를 써서 중복 제거 (fetch join에서 생길 수 있음)N+1 문제 해결:findBySellerIdWithImages
status 조건 없음Page<Product> 타입이라 페이징 처리 가능countBySellerId
// 판매자별 상품 조회
public Page<ProductDto.ProductDetailResponse> getProductsBySeller(
Long sellerId,
ProductStatus status,
Pageable pageable,
Long currentUserId) {
// 판매자 존재 확인
validateSellerExists(sellerId);
Page<Product> productPage;
if (status != null) {
productPage = productRepository.findBySellerIdAndStatusWithImages(sellerId, status, pageable);
} else {
productPage = productRepository.findBySellerIdWithImages(sellerId, pageable);
}
return productPage.map(product -> convertToDetailResponse(product, currentUserId));
}
// 판매자별 상품 수 조회
public int getProductCountBySeller(Long sellerId, ProductStatus status) {
validateSellerExists(sellerId);
if (status != null) {
return (int) productRepository.countBySellerIdAndStatus(sellerId, status);
} else {
return (int) productRepository.countBySellerId(sellerId);
}
}
private void validateSellerExists(Long sellerId) {
if (!userRepository.existsById(sellerId)) {
throw new UserException.UserNotFoundException(sellerId);
}
}
getProductsBySellerDTO 형태로 변환getProductCountBySellerstatus가 null이면 전체 개수 조회validateSellerExistsDB에 없는 경우, 커스텀 예외를 던져서 처리