채팅 후 판매 완료 처리

뚜우웅이·2025년 6월 1일

캡스톤 디자인

목록 보기
34/35

Product

ProductDto


    @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를 생성한다.

ProductStatusService

@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

    • 판매자의 상품인지 검증
    • 이미 판매된 상품인지 확인하고 예외 처리
    • 구매자 존재 여부와 채팅방의 유효성을 검증
    • 상품을 판매완료 상태(SOLD_OUT)로 변경하고 구매자를 설정
    • 채팅방에 거래 완료 시스템 메시지를 전송
  • cancelProductSold

    • 판매자의 상품인지 검증
    • 판매 완료 상태가 아닐 경우 예외 처리
    • 구매자가 존재하는지 확인하고, 리뷰가 이미 작성된 경우 취소가 불가능하므로 예외 처리.
    • 상품 상태를 다시 판매 가능 상태로 변경하고 구매자를 null로 설정
    • 관련된 채팅방에 판매 취소 시스템 메시지를 전송

판매완료 처리와 취소 처리에 채팅을 했던 사용자가 있는 경우에 진행할 수 있도록 수정하고 처리가 되면 채팅방에 메시지가 가도록 설정해줬다.

ProductException

    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");
        }
    }

Chat

ChatRoomRepository

List<ChatRoom> findByProductId(Long productId);

상품 ID로 채팅방을 찾을 수 있게 코드를 추가해준다.

ChatRoomService

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 형태로 변환하여 리턴한다.

ChatController

@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의 엔드포인트를 정의한 컨트롤러 메서드다.

profile
공부하는 초보 개발자

0개의 댓글