@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "buyer_id")
private User buyer;
@Column
private LocalDateTime soldDate; // 판매완료 날짜
// 상품 판매완료 처리 메서드
public void markAsSold(User buyer) {
this.status = ProductStatus.SOLD_OUT;
this.buyer = buyer;
this.soldDate = LocalDateTime.now();
this.stock = 0;
}
// 판매 취소 메서드 추가
public void cancelSold() {
this.status = ProductStatus.ACTIVE;
this.buyer = null;
this.soldDate = null;
this.stock = 1; // 중고 상품은 보통 1개
}
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
// 판매자 ID와 상품 상태로 상품 목록 조회
Page<Product> findBySellerIdAndStatus(Long sellerId, ProductStatus status, Pageable pageable);
// 판매자 ID와 상품 상태로 상품 개수 조회
long countBySellerIdAndStatus(Long sellerId, ProductStatus status);
// 구매자 ID로 상품 목록 조회
Page<Product> findByBuyerIdOrderBySoldDateDesc(Long buyerId, Pageable pageable);
// 구매자 ID로 상품 개수 조회
long countByBuyerId(Long buyerId);
}
단순한 페이징 및 정렬 기능은 QueryDSL 없이도 잘 작동한다.
// 판매 완료 처리
@Transactional
public ProductDto.ProductResponse markProductAsSold(Long sellerId, Long productId, Long buyerId) {
Product product = getProductWithSellerCheck(productId, sellerId);
if (product.getStatus() == ProductStatus.SOLD_OUT) {
throw new IllegalStateException("이미 판매 완료된 상품입니다.");
}
User buyer = userRepository.findById(buyerId)
.orElseThrow(() -> new UserException.UserNotFoundException(buyerId));
product.markAsSold(buyer);
log.info("상품 판매완료 처리: 상품 ID {}, 판매자 ID {}, 구매자 ID {}", productId, sellerId, buyerId);
return ProductDto.ProductResponse.from(product);
}
// 판매완료 취소 처리 메서드
@Transactional
public ProductDto.ProductResponse cancelProductSold(Long sellerId, Long productId) {
Product product = getProductWithSellerCheck(productId, sellerId);
if (product.getStatus() != ProductStatus.SOLD_OUT) {
throw new IllegalStateException("판매 완료 상태가 아닌 상품은 취소할 수 없습니다.");
}
product.cancelSold();
log.info("판매완료 취소 처리: 상품 ID {}, 판매자 ID {}", productId, sellerId);
return ProductDto.ProductResponse.from(product);
}
markProductAsSold
getProductWithSellerCheck()를 통해 상품을 조회하고 현재 사용자가 판매자인지 확인한다.product.markAsSold(buyer)를 호출하여 상품을 판매 완료 상태로 변경하고 구매자를 설정한다.cancelProductSold
getProductWithSellerCheck()를 통해 상품을 조회하고 현재 사용자가 판매자인지 확인한다.product.cancelSold()를 호출하여 상품을 다시 판매 중 상태로 변경한다.@Operation(summary = "상품 판매완료 처리", description = "상품을 판매완료 상태로 변경하고 구매자를 지정합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "판매완료 처리 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "권한 없음"),
@ApiResponse(responseCode = "404", description = "상품을 찾을 수 없음")
})
@PostMapping("/{productId}/sold")
public ResponseEntity<ResponseDTO<ProductDto.ProductResponse>> markProductAsSold(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "상품 ID", required = true) @PathVariable Long productId,
@Parameter(description = "구매자 ID", required = true) @RequestParam Long buyerId) {
log.info("상품 판매완료 처리 요청: 상품 ID {}, 판매자 ID {}, 구매자 ID {}",
productId, userDetails.getUserId(), buyerId);
ProductDto.ProductResponse response = productService.markProductAsSold(
userDetails.getUserId(), productId, buyerId);
return ResponseEntity.ok(ResponseDTO.success(response, "상품이 판매완료 처리되었습니다."));
}
@Operation(summary = "판매완료 취소", description = "판매완료 상태의 상품을 다시 판매중 상태로 변경합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "판매완료 취소 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "권한 없음"),
@ApiResponse(responseCode = "404", description = "상품을 찾을 수 없음")
})
@PostMapping("/{productId}/cancel-sold")
public ResponseEntity<ResponseDTO<ProductDto.ProductResponse>> cancelProductSold(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "상품 ID", required = true) @PathVariable Long productId) {
log.info("판매완료 취소 요청: 상품 ID {}, 판매자 ID {}", productId, userDetails.getUserId());
ProductDto.ProductResponse response = productService.cancelProductSold(
userDetails.getUserId(), productId);
return ResponseEntity.ok(ResponseDTO.success(response, "판매완료가 취소되었습니다."));
}
상품 판매 완료 처리 시 @RequestParam Long buyerId: 구매자 ID (쿼리 파라미터)을 입력해줘야 한다.
public class UserProfileDto {
// 판매 상품 내역 DTO
@Builder
public record SellingProductResponse(
Long productId,
String name,
Long price,
String category,
String status,
String thumbnailUrl,
String buyerName,
LocalDateTime soldDate,
LocalDateTime createdDate
) {}
// 판매 내역 페이지 응답 DTO
@Builder
public record SellingHistoryResponse(
List<SellingProductResponse> activeProducts,
List<SellingProductResponse> soldProducts,
int totalProductCount
) {}
// 구매 내역 DTO
@Builder
public record PurchaseItem(
Long productId,
String ProductName,
Long price,
String category,
String thumbnailUrl,
String sellerName,
LocalDateTime purchaseDate
) {}
// 구매 내역 페이지 응답 DTO
@Builder
public record PurchaseHistoryResponse(
List<PurchaseItem> purchases,
int totalPurchaseCount
) {}
// 프로필 요약 정보 DTO
@Builder
public record ProfileSummaryResponse(
Long userId,
String email,
String name,
int totalSellingCount,
int totalSoldCount,
int totalPurchaseCount,
double averageRating,
LocalDateTime joinDate
) {}
}
SellingProductResponse
판매 중인 또는 판매된 단일 상품 정보를 담는 DTO
SellingHistoryResponse
사용자의 전체 판매 내역을 담는 DTO
PurchaseItem
구매한 단일 상품 정보를 담는 DTO
PurchaseHistoryResponse
사용자의 전체 구매 내역을 담는 DTO
ProfileSummaryResponse
사용자 프로필의 요약 정보를 담는 DTO
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserProfileService {
private final UserRepository userRepository;
private final ProductRepository productRepository;
// 사용자의 프로필 요약 정보를 조회
public UserProfileDto.ProfileSummaryResponse getProfileSummary(Long userId) {
User user = getUser(userId);
// 판매 중인 상품 수
long activeProductCount = productRepository.countBySellerIdAndStatus(userId, ProductStatus.ACTIVE);
// 판매 완료된 상품 수
long soldProductCount = productRepository.countBySellerIdAndStatus(userId, ProductStatus.SOLD_OUT);
// 구매한 상품 수
long purchaseCount = productRepository.countByBuyerId(userId);
// TODO: 평균 평점 계산 로직
double averageRating = 0.0;
return UserProfileDto.ProfileSummaryResponse.builder()
.userId(user.getId())
.email(user.getEmail())
.name(user.getName())
.totalSellingCount((int) activeProductCount)
.totalSoldCount((int) soldProductCount)
.totalPurchaseCount((int) purchaseCount)
.averageRating(averageRating)
.joinDate(user.getCreatedDate())
.build();
}
// 사용자의 판매 상품 내역을 조회
public UserProfileDto.SellingHistoryResponse getSellingHistory(Long userId, Pageable pageable) {
// 판매중인 상품 조회
Page<Product> activeProductsPage = productRepository.findBySellerIdAndStatus(userId, ProductStatus.ACTIVE, pageable);
// 판매 완료된 상품 조회
Page<Product> soldProductsPage = productRepository.findBySellerIdAndStatus(userId, ProductStatus.SOLD_OUT, pageable);
// DTO 변환
List<UserProfileDto.SellingProductResponse> activeProductResponses =
activeProductsPage.stream().map(this::mapToSellingProductResponse).collect(Collectors.toList());
List<UserProfileDto.SellingProductResponse> soldProductResponses =
soldProductsPage.stream().map(this::mapToSellingProductResponse).collect(Collectors.toList());
return UserProfileDto.SellingHistoryResponse.builder()
.activeProducts(activeProductResponses)
.soldProducts(soldProductResponses)
.totalProductCount((int) (activeProductsPage.getTotalElements() +
soldProductsPage.getTotalElements()))
.build();
}
// 사용자의 구매 내역 조회 (Product 엔터티의 buyer 필드 기준)
public UserProfileDto.PurchaseHistoryResponse getPurchaseHistory(Long userId, Pageable pageable) {
// 사용자가 구매한 상품 목록 조회 (buyer_id 기준)
Page<Product> purchasedProductsPage = productRepository.findByBuyerIdOrderBySoldDateDesc(userId, pageable);
// DTO 변환
List<UserProfileDto.PurchaseItem> purchaseItems = purchasedProductsPage.getContent().stream()
.map(this::mapToPurchaseItem)
.collect(Collectors.toList());
return UserProfileDto.PurchaseHistoryResponse.builder()
.purchases(purchaseItems)
.totalPurchaseCount((int) purchasedProductsPage.getTotalElements())
.build();
}
// Product 엔터티를 SellingProductResponse DTO로 변환
private UserProfileDto.SellingProductResponse mapToSellingProductResponse(Product product) {
String buyerName = product.getBuyer() != null ? product.getBuyer().getName() : null;
return UserProfileDto.SellingProductResponse.builder()
.productId(product.getId())
.name(product.getName())
.price(product.getPrice())
.category(product.getCategory().getDisplayName())
.status(product.getStatus().getDisplayName())
.thumbnailUrl(product.getRepresentativeThumbnailUrl())
// .viewCount(0) // TODO: 조회수 구현 시 추가
// .likeCount(0) // TODO: 찜하기 기능 구현 시 추가
.buyerName(buyerName)
.soldDate(product.getSoldDate())
.createdDate(product.getCreatedDate())
.build();
}
// 구매한 상품을 PurchaseHistoryItem DTO로 변환
private UserProfileDto.PurchaseItem mapToPurchaseItem(Product product) {
return UserProfileDto.PurchaseItem.builder()
.productId(product.getId())
.productName(product.getName())
.price(product.getPrice())
.category(product.getCategory().getDisplayName())
.thumbnailUrl(product.getRepresentativeThumbnailUrl())
.sellerName(product.getSeller().getName())
.purchaseDate(product.getSoldDate())
.build();
}
private User getUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserException.UserNotFoundException(userId));
}
}
주요 기능
프로필 요약 정보 조회 (getProfileSummary)
사용자의 기본 정보와 요약 통계를 제공
판매 내역 조회 (getSellingHistory)
사용자가 판매 중이거나 판매 완료한 상품 목록을 조회
구매 내역 조회 (getPurchaseHistory)
사용자가 구매한 상품 목록을 조회
유틸리티 메서드
mapToSellingProductResponse
Product 엔티티를 SellingProductResponse DTO로 변환하는 메서드
mapToPurchaseItem
Product 엔티티를 PurchaseItem DTO로 변환하는 메서드
@Slf4j
@RestController
@RequestMapping("/api/users/profile")
@RequiredArgsConstructor
@Tag(name = "사용자 프로필", description = "사용자 프로필 관련 API")
public class UserProfileController {
private final UserProfileService userProfileService;
@Operation(summary = "프로필 요약 정보 조회", description = "로그인한 사용자의 프로필 요약 정보를 조회합니다.")
@ApiResponse(responseCode = "200", description = "프로필 요약 정보 조회 성공")
@GetMapping("/summary")
public ResponseEntity<ResponseDTO<UserProfileDto.ProfileSummaryResponse>> getProfileSummary(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {
log.info("프로필 요약 정보 조회: 사용자 ID {}", userDetails.getUserId());
UserProfileDto.ProfileSummaryResponse response = userProfileService.getProfileSummary(userDetails.getUserId());
return ResponseEntity.ok(ResponseDTO.success(response));
}
@Operation(summary = "판매 내역 조회", description = "로그인한 사용자의 판매 내역을 조회합니다.")
@ApiResponse(responseCode = "200", description = "판매 내역 조회 성공")
@GetMapping("/selling")
public ResponseEntity<ResponseDTO<UserProfileDto.SellingHistoryResponse>> getSellingHistory(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "페이지네이션 정보")
@PageableDefault(size = 10) Pageable pageable) {
log.info("판매 내역 조회: 사용자 ID {}", userDetails.getUserId());
UserProfileDto.SellingHistoryResponse response = userProfileService.getSellingHistory(userDetails.getUserId(), pageable);
return ResponseEntity.ok(ResponseDTO.success(response));
}
@Operation(summary = "구매 내역 조회", description = "로그인한 사용자의 구매 내역을 조회합니다.")
@ApiResponse(responseCode = "200", description = "구매 내역 조회 성공")
@GetMapping("/me/purchases")
public ResponseEntity<ResponseDTO<UserProfileDto.PurchaseHistoryResponse>> getPurchaseHistory(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "페이지네이션 정보")
@PageableDefault(size = 10) Pageable pageable) {
log.info("구매 내역 조회: 사용자 ID {}", userDetails.getUserId());
UserProfileDto.PurchaseHistoryResponse response =
userProfileService.getPurchaseHistory(userDetails.getUserId(), pageable);
return ResponseEntity.ok(ResponseDTO.success(response));
}
@Operation(summary = "다른 사용자 프로필 조회", description = "다른 사용자의 프로필 정보를 조회합니다. (제한된 정보만 제공)")
@ApiResponse(responseCode = "200", description = "사용자 프로필 조회 성공")
@GetMapping("/{userId}")
public ResponseEntity<ResponseDTO<UserProfileDto.ProfileSummaryResponse>> getUserProfile(
@Parameter(description = "조회할 사용자 ID", required = true) @PathVariable Long userId) {
log.info("사용자 프로필 조회: 사용자 ID {}", userId);
UserProfileDto.ProfileSummaryResponse response = userProfileService.getProfileSummary(userId);
return ResponseEntity.ok(ResponseDTO.success(response));
}
}
주요 API 엔드포인트
프로필 요약 정보 조회 (GET /api/users/profile/summary)
CustomUserDetails에서 추출판매 내역 조회 (GET /api/users/profile/selling)
구매 내역 조회 (GET /api/users/profile/me/purchases)
다른 사용자 프로필 조회 (GET /api/users/profile/{userId})
판매자 id는 1
구매자 id는 2







다시 판매자로 로그인을 해서 판매완료 취소 요청을 보낸다.

구매자로 로그인하여 확인한다.

