diff --git a/src/main/java/com/tenure/domain/chat/controller/ChatController.java b/src/main/java/com/tenure/domain/chat/controller/ChatController.java index 522f68f5..3b8fcd52 100644 --- a/src/main/java/com/tenure/domain/chat/controller/ChatController.java +++ b/src/main/java/com/tenure/domain/chat/controller/ChatController.java @@ -39,7 +39,7 @@ public class ChatController { public BaseResponse findOrCreateChatRoom(@RequestBody @Valid ChatRoomRequest chatRoomRequest) { ChatRoomResponse chatRoomResponse = chatRoomService - .findOrCreateChatRoom(currentUserProvider.getCurrentUserId(), chatRoomRequest.getItemId()); + .findOrCreateChatRoom(currentUserProvider.getCurrentUserId(), chatRoomRequest.getItemId(), chatRoomRequest.getPurchaseOfferId()); return BaseResponse.success(chatRoomResponse); diff --git a/src/main/java/com/tenure/domain/chat/dto/request/ChatRoomRequest.java b/src/main/java/com/tenure/domain/chat/dto/request/ChatRoomRequest.java index 14f374f8..bce07bce 100644 --- a/src/main/java/com/tenure/domain/chat/dto/request/ChatRoomRequest.java +++ b/src/main/java/com/tenure/domain/chat/dto/request/ChatRoomRequest.java @@ -12,4 +12,7 @@ public class ChatRoomRequest { @NotNull(message = "아이템 id는 필수입니다.") private Long itemId; + + // 판매자가 채팅방을 생성할 때 사용. 구매자 입장에서는 null. + private Long purchaseOfferId; } diff --git a/src/main/java/com/tenure/domain/chat/dto/response/ChatRoomResponse.java b/src/main/java/com/tenure/domain/chat/dto/response/ChatRoomResponse.java index 4e72e6f0..e46ab658 100644 --- a/src/main/java/com/tenure/domain/chat/dto/response/ChatRoomResponse.java +++ b/src/main/java/com/tenure/domain/chat/dto/response/ChatRoomResponse.java @@ -4,6 +4,9 @@ import com.tenure.domain.item.entity.Item; import com.tenure.domain.product.entity.Product; import com.tenure.domain.product.enums.ProductStatus; + +import static com.tenure.domain.product.enums.ProductStatus.ON_SALE; +import static com.tenure.domain.product.enums.ProductStatus.TRADING; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -39,7 +42,8 @@ public class ChatRoomResponse { public static ChatRoomResponse from( ChatRoom chatRoom, Item item, Product product, Long currentUserId, Long tradeId, Long purchaseIntentId, - Long purchaseOfferId, boolean isBlocked, boolean isOpponentExited) + Long purchaseOfferId, boolean isBlocked, boolean isOpponentExited, + Integer offerPrice) { boolean isBuyer = currentUserId.equals(chatRoom.getBuyer().getId()); @@ -56,7 +60,12 @@ public static ChatRoomResponse from( : chatRoom.getBuyer().getProfileImageUrl(); ProductStatus productStatus = product != null ? product.getProductStatus() : null; - Integer price = product != null ? product.getPrice() : null; + + + // 판매중/거래중이면 상품 가격, 그 외(미판매 + product없음)는 제안 금액(없으면 null) + Integer price = (product != null && (product.getProductStatus() == ON_SALE || product.getProductStatus() == TRADING)) + ? product.getPrice() + : offerPrice; Long productId = product != null ? product.getId() : null; return new ChatRoomResponse(chatRoom.getId(), opponentUserId, opponentUsername, opponentProfileImage, item.getId(), diff --git a/src/main/java/com/tenure/domain/chat/service/ChatRoomService.java b/src/main/java/com/tenure/domain/chat/service/ChatRoomService.java index 4dbb6b40..fa02a575 100644 --- a/src/main/java/com/tenure/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/tenure/domain/chat/service/ChatRoomService.java @@ -21,8 +21,10 @@ import com.tenure.domain.product.entity.Product; import com.tenure.domain.product.exception.ProductErrorCode; import com.tenure.domain.product.repository.ProductRepository; +import com.tenure.domain.purchase.entity.PurchaseOffer; import com.tenure.domain.purchase.enums.PurchaseIntentStatus; import com.tenure.domain.purchase.enums.PurchaseOfferStatus; +import com.tenure.domain.purchase.exception.PurchaseOfferErrorCode; import com.tenure.domain.purchase.repository.PurchaseIntentRepository; import com.tenure.domain.purchase.repository.PurchaseOfferRepository; import com.tenure.domain.trade.entity.Trade; @@ -80,9 +82,9 @@ public class ChatRoomService { // 채팅방 조회 / 생성 @Transactional - public ChatRoomResponse findOrCreateChatRoom(Long buyerId, Long itemId) { + public ChatRoomResponse findOrCreateChatRoom(Long currentUserId, Long itemId, Long purchaseOfferId) { - log.info("[채팅방 생성/조회] buyerId = {}, itemId = {}", buyerId, itemId); + log.info("[채팅방 생성/조회] currentUserId = {}, itemId = {}", currentUserId, itemId); //아이템 + 주인 한 번에 조회 Item item = itemRepository.findByIdWithOwner(itemId) @@ -93,16 +95,42 @@ public ChatRoomResponse findOrCreateChatRoom(Long buyerId, Long itemId) { User owner = item.getOwner(); - //구매자(사용자) 조회 - User buyer = userRepository.findById(buyerId) - .orElseThrow(() -> { - log.warn("[채팅방 생성/조회] 해당 구매자는 존재하지 않습니다. buyerId = {}", buyerId); - return new CustomException(UserErrorCode.USER_NOT_FOUND); - }); + Long buyerId; + User buyer; + + // 판매자라면 + if (currentUserId.equals(owner.getId())) { + // 판매자가 채팅 시작 — purchaseOfferId로 구매자 특정 + if (purchaseOfferId == null) { + log.warn("[채팅방 생성/조회] 판매자가 채팅방 생성 시 purchaseOfferId 필요. currentUserId = {}", currentUserId); + throw new CustomException(ChatErrorCode.CHAT_CREATION_NOT_ALLOWED); + } + PurchaseOffer offer = purchaseOfferRepository.findByIdWithUsers(purchaseOfferId) + .orElseThrow(() -> { + log.warn("[채팅방 생성/조회] 구매 제안을 찾을 수 없습니다. purchaseOfferId = {}", purchaseOfferId); + return new CustomException(PurchaseOfferErrorCode.PURCHASE_OFFER_NOT_FOUND); + }); + + // 검증 + boolean valid = offer.getItem().getId().equals(itemId) + && offer.getOwner().getId().equals(currentUserId) + && (offer.getStatus() == SENT || offer.getStatus() == ACCEPTED); + + if (!valid) { + log.warn("[채팅방 생성/조회] 유효하지 않은 구매 제안. purchaseOfferId = {}", purchaseOfferId); + throw new CustomException(ChatErrorCode.CHAT_CREATION_NOT_ALLOWED); + } - if(buyerId.equals(owner.getId())) { - log.warn("[채팅방 생성 / 조회] 본인의 아이템에는 채팅방을 생성 할 수없습니다. buyerId = {}, ownerId = {}", buyerId, owner.getId()); - throw new CustomException(ChatErrorCode.CHAT_CREATION_NOT_ALLOWED); + buyer = offer.getProposer(); + buyerId = buyer.getId(); + } else { + // 구매자가 채팅 시작 + buyerId = currentUserId; + buyer = userRepository.findById(buyerId) + .orElseThrow(() -> { + log.warn("[채팅방 생성/조회] 해당 구매자는 존재하지 않습니다. buyerId = {}", buyerId); + return new CustomException(UserErrorCode.USER_NOT_FOUND); + }); } // 양방향 차단 한 번에 조회 @@ -172,13 +200,17 @@ public ChatRoomResponse findOrCreateChatRoom(Long buyerId, Long itemId) { .findIdByBuyerIdAndSellerIdAndProductIdAndStatus(buyerId, owner.getId(), product.getId(), PurchaseIntentStatus.SENT) .orElse(null); - Long purchaseOfferId = purchaseOfferRepository + Long sentPurchaseOfferId = purchaseOfferRepository .findIdByProposerIdAndOwnerIdAndItemIdAndStatus(buyerId, owner.getId(), itemId, SENT) .orElse(null); + Integer offerPrice = purchaseOfferRepository + .findOfferPriceByProposerIdAndOwnerIdAndItemIdAndStatusIn(buyerId, owner.getId(), itemId, List.of(SENT, ACCEPTED)) + .orElse(null); + // 아이템 상세에서 바로 들어온 경우 처음엔 isOpponentExited false 고정 return ChatRoomResponse - .from(chatRoom, item, product, buyerId, tradeId, purchaseIntentId, purchaseOfferId, ownerBlockedBuyer, false); + .from(chatRoom, item, product, currentUserId, tradeId, purchaseIntentId, sentPurchaseOfferId, ownerBlockedBuyer, false, offerPrice); } // 채팅방 목록 조회 @@ -254,6 +286,10 @@ public ChatRoomResponse enterChatroom(Long currentUserId, Long chatRoomId) { .findIdByProposerIdAndOwnerIdAndItemIdAndStatus(buyerId, sellerId, item.getId(), SENT) .orElse(null); + Integer offerPrice = purchaseOfferRepository + .findOfferPriceByProposerIdAndOwnerIdAndItemIdAndStatusIn(buyerId, sellerId, item.getId(), List.of(SENT, ACCEPTED)) + .orElse(null); + // 상대방이 나를 차단했는지 여부 boolean isBlocked = userBlockRepository.isBlocked(opponentId, currentUserId); @@ -267,7 +303,7 @@ public ChatRoomResponse enterChatroom(Long currentUserId, Long chatRoomId) { .ifPresent(Notification::markRead); return ChatRoomResponse - .from(chatRoom, item, product, currentUserId, tradeId, purchaseIntentId, purchaseOfferId, isBlocked, isOpponentExited); + .from(chatRoom, item, product, currentUserId, tradeId, purchaseIntentId, purchaseOfferId, isBlocked, isOpponentExited, offerPrice); } //채팅방 접속 시 unreadCount 업데이트 diff --git a/src/main/java/com/tenure/domain/purchase/repository/PurchaseOfferRepository.java b/src/main/java/com/tenure/domain/purchase/repository/PurchaseOfferRepository.java index cc2c78c3..34a7cab0 100644 --- a/src/main/java/com/tenure/domain/purchase/repository/PurchaseOfferRepository.java +++ b/src/main/java/com/tenure/domain/purchase/repository/PurchaseOfferRepository.java @@ -36,6 +36,9 @@ List findExpiredSentTargets( @Query("select offer from PurchaseOffer offer where offer.id = :offerId") Optional findByIdForUpdate(@Param("offerId") Long offerId); + @Query("select offer from PurchaseOffer offer join fetch offer.item join fetch offer.owner join fetch offer.proposer where offer.id = :offerId") + Optional findByIdWithUsers(@Param("offerId") Long offerId); + @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" select offer @@ -242,6 +245,13 @@ Optional findIdByProposerIdAndOwnerIdAndItemIdAndStatus( @Param("itemId") Long itemId, @Param("status") PurchaseOfferStatus status); + @Query("select po.offerPrice from PurchaseOffer po where po.proposer.id = :proposerId and po.owner.id = :ownerId and po.item.id = :itemId and po.status in :statuses") + Optional findOfferPriceByProposerIdAndOwnerIdAndItemIdAndStatusIn( + @Param("proposerId") Long proposerId, + @Param("ownerId") Long ownerId, + @Param("itemId") Long itemId, + @Param("statuses") List statuses); + @Query("select count(po) > 0 from PurchaseOffer po where po.proposer.id = :proposerId and po.owner.id = :ownerId and po.item.id = :itemId and po.status in :statuses") boolean existsByProposerIdAndOwnerIdAndItemIdAndStatusIn( @Param("proposerId") Long proposerId,