@Schema(description = "판매 완료 처리 요청")
public record SaleCompleteRequest(
@Schema(description = "구매자 ID", example = "2")
@NotNull(message = "구매자 ID는 필수입니다.")
Long buyerId,
@Schema(description = "채팅방 ID", example = "1")
@NotNull(message = "채팅방 ID는 필수입니다.")
Long chatRoomId
) {}
판매 완료 처리시 채팅방의 id 값을 이용하도록 DTO를 생성한다.
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProductStatusService {
private final ProductManagementService productManagementService;
private final UserRepository userRepository;
private final ReviewRepository reviewRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final SimpMessagingTemplate simpMessagingTemplate;
// 판매 완료 처리
@Transactional
public ProductDto.ProductResponse markProductAsSold(Long sellerId, Long productId, Long buyerId, Long chatRoomId) {
Product product = productManagementService.getProductWithSellerCheck(productId, sellerId);
if (product.getStatus() == ProductStatus.SOLD_OUT) {
throw new ProductException.AlreadySoldProductException();
}
User buyer = userRepository.findById(buyerId)
.orElseThrow(() -> new UserException.UserNotFoundException(buyerId));
// 채팅방 확인
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow(
() -> new ChatException.ChatRoomNotFoundException(chatRoomId));
// 채팅방 참가자 확인 (판매자와 구매자가 맞는지)
if (!chatRoom.getSeller().getId().equals(sellerId) || !chatRoom.getBuyer().getId().equals(buyerId)) {
throw new ChatException.ChatRoomAccessDeniedException();
}
// 채팅방의 상품이 판매하려는 상품과 일치하는지 확인
if (!chatRoom.getProduct().getId().equals(productId)) {
throw new ProductException.ProductMismatchException();
}
// 상품 판매완료 처리
product.markAsSold(buyer);
// 채팅방에 시스템 메시지 전송
sendSaleCompletionMessage(chatRoom, buyer);
log.info("상품 판매완료 처리: 상품 ID {}, 판매자 ID {}, 구매자 ID {}, 채팅방 ID {}",
productId, sellerId, buyerId, chatRoomId);
return ProductDto.ProductResponse.from(product);
}
// 판매완료 취소 처리 메서드
@Transactional
public ProductDto.ProductResponse cancelProductSold(Long sellerId, Long productId) {
Product product = productManagementService.getProductWithSellerCheck(productId, sellerId);
if (product.getStatus() != ProductStatus.SOLD_OUT) {
throw new ProductException.NotSoldProductException();
}
// 구매자 정보 백업 (취소 후에는 null이 되기 때문)
User buyer = product.getBuyer();
if (buyer == null) {
throw new ProductException.BuyerNotFoundException();
}
// 리뷰가 작성되었는지 확인
boolean reviewExists = reviewRepository.findByProduct(product).isPresent();
if (reviewExists) {
throw new ProductException.CannotCancelSoldProductException("이미 리뷰가 작성된 상품은 판매완료 취소가 불가능합니다.");
}
// 판매완료 취소 처리
product.cancelSold();
// 이 상품에 대한 채팅방 중 구매자와의 채팅방 찾기
List<ChatRoom> chatRooms = chatRoomRepository.findByProductId(productId);
chatRooms.stream()
.filter(chatRoom -> chatRoom.getBuyer().getId().equals(buyer.getId()))
.findFirst()
.ifPresent(chatRoom -> {
// 취소 알림 메시지 전송
sendSaleCancellationMessage(chatRoom, product);
});
log.info("판매완료 취소 처리: 상품 ID {}, 판매자 ID {}", productId, sellerId);
return ProductDto.ProductResponse.from(product);
}
// 판매 완료 시스템 메시지 전송
private void sendSaleCompletionMessage(ChatRoom chatRoom, User buyer) {
try {
// 시스템 메시지 생성
String content = String.format("거래가 완료되었습니다. 구매자: %s", buyer.getName());
// 채팅 메시지 저장
ChatMessage systemMessage = ChatMessage.builder()
.chatRoom(chatRoom)
.sender(chatRoom.getSeller())
.content(content)
.messageType(MessageType.TEXT)
.build();
chatMessageRepository.save(systemMessage);
// 채팅방 마지막 메시지 업데이트
chatRoom.updateLastMessage(content, LocalDateTime.now());
// WebSocket으로 메시지 전송
simpMessagingTemplate.convertAndSend(
"/topic/chat/" + chatRoom.getId(),
ChatResponseDto.MessageResponse.from(systemMessage)
);
log.info("판매 완료 시스템 메시지 전송 성공: 채팅방 ID {}", chatRoom.getId());
} catch (Exception e) {
log.error("판매 완료 시스템 메시지 전송 실패: 채팅방 ID {}, 오류: {}",
chatRoom.getId(), e.getMessage());
}
}
// 판매 취소 시스템 메시지 전송
private void sendSaleCancellationMessage(ChatRoom chatRoom, Product product) {
try {
// 시스템 메시지 생성
String content = String.format("판매자가 거래를 취소했습니다. 상품: %s", product.getName());
// 채팅 메시지 저장
ChatMessage systemMessage = ChatMessage.builder()
.chatRoom(chatRoom)
.sender(chatRoom.getSeller()) // 판매자를 발신자로 설정
.content(content)
.messageType(MessageType.SYSTEM)
.build();
chatMessageRepository.save(systemMessage);
// 채팅방 마지막 메시지 업데이트
chatRoom.updateLastMessage(content, LocalDateTime.now());
// WebSocket으로 메시지 전송 (실시간 알림)
simpMessagingTemplate.convertAndSend(
"/topic/chat/" + chatRoom.getId(),
ChatResponseDto.MessageResponse.from(systemMessage)
);
log.info("판매 취소 시스템 메시지 전송 성공: 채팅방 ID {}", chatRoom.getId());
} catch (Exception e) {
log.error("판매 취소 시스템 메시지 전송 실패: 채팅방 ID {}, 오류: {}",
chatRoom.getId(), e.getMessage());
// 실패해도 판매 취소 처리는 진행
}
}
}
markProductAsSold
cancelProductSold
null로 설정판매완료 처리와 취소 처리에 채팅을 했던 사용자가 있는 경우에 진행할 수 있도록 수정하고 처리가 되면 채팅방에 메시지가 가도록 설정해줬다.
public static class BuyerNotFoundException extends ProductException {
public BuyerNotFoundException() {
super("구매자 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "BUYER_NOT_FOUND");
}
}
public static class ProductMismatchException extends ProductException {
public ProductMismatchException() {
super("채팅방의 상품과 판매하려는 상품이 일치하지 않습니다.", HttpStatus.BAD_REQUEST, "PRODUCT_MISMATCH");
}
}
List<ChatRoom> findByProductId(Long productId);
상품 ID로 채팅방을 찾을 수 있게 코드를 추가해준다.
public ChatResponseDto.ChatRoomListResponse getProductChatRooms(Long productId, Long sellerId) {
// 상품 존재 여부 확인
Product product = getProduct(productId);
// 판매자 권한 확인
if (!product.getSeller().getId().equals(sellerId)) {
throw new ProductException.ProductAccessDeniedException();
}
// 해당 상품의 모든 채팅방 조회
List<ChatRoom> chatRooms = chatRoomRepository.findByProductId(productId);
return ChatResponseDto.ChatRoomListResponse.builder()
.chatRooms(chatRooms.stream()
.map(ChatResponseDto.ChatRoomResponse::from)
.toList())
.totalCount(chatRooms.size())
.build();
}
상품과 연관된 모든 채팅방을 데이터베이스에서 조회한 뒤 DTO 형태로 변환하여 리턴한다.
@Operation(summary = "상품별 채팅방 목록 조회", description = "특정 상품에 대한 모든 채팅방 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "채팅방 목록 조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "접근 권한 없음"),
@ApiResponse(responseCode = "404", description = "상품을 찾을 수 없음")
})
@GetMapping("/products/{productId}/chat-rooms")
public ResponseEntity<ResponseDTO<ChatResponseDto.ChatRoomListResponse>> getProductChatRooms(
@Parameter(description = "상품 ID", required = true) @PathVariable Long productId,
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {
log.info("상품별 채팅방 목록 조회 요청: 상품 ID {}, 사용자 ID {}", productId, userDetails.getUserId());
ChatResponseDto.ChatRoomListResponse response = chatRoomService.getProductChatRooms(productId, userDetails.getUserId());
return ResponseEntity.ok(ResponseDTO.success(response, "상품 채팅방 목록을 성공적으로 조회했습니다."));
}
특정 상품에 대한 모든 채팅방 목록을 조회하는 REST API의 엔드포인트를 정의한 컨트롤러 메서드다.