diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java index efb3d78d6..8e91c6c0a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java @@ -9,6 +9,7 @@ import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; @@ -22,6 +23,7 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +37,7 @@ public class ChatRoomService { private static final Long DEFAULT_CHAT_ROOM_ID = null; + private final ApplicationEventPublisher messageLogEventPublisher; private final ChatRoomRepository chatRoomRepository; private final ChatRoomAndImageRepository chatRoomAndImageRepository; private final ChatRoomAndMessageAndImageRepository chatRoomAndMessageAndImageRepository; @@ -51,9 +54,15 @@ public Long create(final Long userId, final CreateChatRoomDto chatRoomDto) { ); return chatRoomRepository.findChatRoomIdByAuctionId(findAuction.getId()) - .orElseGet(() -> - persistChatRoom(findUser, findAuction).getId() - ); + .orElseGet(() -> createChatRoom(findUser, findAuction)); + } + + private Long createChatRoom(final User findUser, final Auction findAuction) { + final ChatRoom persistChatRoom = persistChatRoom(findUser, findAuction); + + messageLogEventPublisher.publishEvent(new CreateReadMessageLogEvent(persistChatRoom)); + + return persistChatRoom.getId(); } private ChatRoom persistChatRoom(final User user, final Auction auction) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java new file mode 100644 index 000000000..5c67a1089 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java @@ -0,0 +1,39 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LastReadMessageLogEventListener { + + private final LastReadMessageLogService lastReadMessageLogService; + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { + try { + lastReadMessageLogService.create(createReadMessageLogEvent); + } catch (final IllegalArgumentException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { + try { + lastReadMessageLogService.update(updateReadMessageLogEvent); + } catch (final ReadMessageLogNotFoundException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java new file mode 100644 index 000000000..abba0d301 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LastReadMessageLogService { + + private final ReadMessageLogRepository readMessageLogRepository; + + @Transactional + public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { + final ChatRoom chatRoom = createReadMessageLogEvent.chatRoom(); + final User buyer = chatRoom.getBuyer(); + final User seller = chatRoom.getAuction().getSeller(); + final ReadMessageLog buyerReadMessageLog = new ReadMessageLog(chatRoom, buyer); + final ReadMessageLog sellerReadMessageLog = new ReadMessageLog(chatRoom, seller); + + readMessageLogRepository.saveAll(List.of(buyerReadMessageLog, sellerReadMessageLog)); + } + + @Transactional + public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { + final User reader = updateReadMessageLogEvent.reader(); + final ChatRoom chatRoom = updateReadMessageLogEvent.chatRoom(); + final ReadMessageLog messageLog = readMessageLogRepository.findBy(reader.getId(), chatRoom.getId()) + .orElseThrow(() -> + new ReadMessageLogNotFoundException( + "메시지 조회 로그가 존재하지 않습니다." + )); + + messageLog.updateLastReadMessage(updateReadMessageLogEvent.lastReadMessage().getId()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java index 35493d0f5..9d9045ecb 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java @@ -3,6 +3,7 @@ import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; @@ -15,7 +16,6 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,10 +25,10 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -@Slf4j public class MessageService { - private final ApplicationEventPublisher messageEventPublisher; + private final ApplicationEventPublisher messageLogEventPublisher; + private final ApplicationEventPublisher messageNotificationEventPublisher; private final MessageRepository messageRepository; private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; @@ -53,16 +53,14 @@ public Long create(final CreateMessageDto dto, final String profileImageAbsolute final Message persistMessage = messageRepository.save(message); - messageEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); + messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); return persistMessage.getId(); } public List readAllByLastMessageId(final ReadMessageRequest request) { - if (!userRepository.existsById(request.messageReaderId())) { - throw new UserNotFoundException("지정한 아이디에 대한 사용자를 찾을 수 없습니다."); - } - + final User reader = userRepository.findById(request.messageReaderId()) + .orElseThrow(() -> new UserNotFoundException("지정한 아이디에 대한 사용자를 찾을 수 없습니다.")); final ChatRoom chatRoom = chatRoomRepository.findById(request.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); @@ -77,6 +75,12 @@ public List readAllByLastMessageId(final ReadMessageRequest requ request.lastMessageId() ); + if (!readMessages.isEmpty()) { + final Message lastReadMessage = readMessages.get(readMessages.size() - 1); + + messageLogEventPublisher.publishEvent(new UpdateReadMessageLogEvent(reader, chatRoom, lastReadMessage)); + } + return readMessages.stream() .map(message -> ReadMessageDto.from(message, chatRoom)) .toList(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java index 36e3d0382..b52e55a9e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java @@ -11,6 +11,7 @@ public record ReadChatRoomWithLastMessageDto( ReadAuctionInChatRoomDto auctionDto, ReadUserInChatRoomDto partnerDto, ReadLastMessageDto lastMessageDto, + Long unreadMessageCount, boolean isChatAvailable ) { @@ -22,12 +23,14 @@ public static ReadChatRoomWithLastMessageDto of( final User partner = chatRoom.calculateChatPartnerOf(findUser); final Message lastMessage = chatRoomAndMessageAndImageDto.message(); final AuctionImage thumbnailImage = chatRoomAndMessageAndImageDto.thumbnailImage(); + final Long unreadMessages = chatRoomAndMessageAndImageDto.unreadMessageCount(); return new ReadChatRoomWithLastMessageDto( chatRoom.getId(), ReadAuctionInChatRoomDto.of(chatRoom.getAuction(), thumbnailImage), ReadUserInChatRoomDto.from(partner), ReadLastMessageDto.from(lastMessage), + unreadMessages, chatRoom.isChatAvailablePartner(partner) ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java new file mode 100644 index 000000000..fec66ac3a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.chat.application.event; + +import com.ddang.ddang.chat.domain.ChatRoom; + +public record CreateReadMessageLogEvent(ChatRoom chatRoom) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java new file mode 100644 index 000000000..8fd1cde98 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.application.event; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.user.domain.User; + +public record UpdateReadMessageLogEvent(User reader, ChatRoom chatRoom, Message lastReadMessage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java new file mode 100644 index 000000000..45eb4623f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.application.exception; + +public class ReadMessageLogNotFoundException extends IllegalArgumentException { + + public ReadMessageLogNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java new file mode 100644 index 000000000..c741228a4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java @@ -0,0 +1,49 @@ +package com.ddang.ddang.chat.domain; + +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "lastReadMessageId"}) +public class ReadMessageLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE}) + @JoinColumn(name = "chat_room_id", nullable = false, foreignKey = @ForeignKey(name = "fk_read_message_log_chat_room")) + private ChatRoom chatRoom; + + @OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE}) + @JoinColumn(name = "reader_id", nullable = false, foreignKey = @ForeignKey(name = "fk_read_message_log_reader")) + private User reader; + + private Long lastReadMessageId = 0L; + + public ReadMessageLog(final ChatRoom chatRoom, final User reader) { + this.chatRoom = chatRoom; + this.reader = reader; + } + + public void updateLastReadMessage(final Long lastReadMessageId) { + this.lastReadMessageId = lastReadMessageId; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java index 2e7e6ea6b..b31168c91 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java @@ -4,5 +4,10 @@ import com.ddang.ddang.chat.domain.Message; import com.ddang.ddang.image.domain.AuctionImage; -public record ChatRoomAndMessageAndImageDto(ChatRoom chatRoom, Message message, AuctionImage thumbnailImage) { +public record ChatRoomAndMessageAndImageDto( + ChatRoom chatRoom, + Message message, + AuctionImage thumbnailImage, + Long unreadMessageCount +) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java new file mode 100644 index 000000000..0da187e3a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java @@ -0,0 +1,13 @@ +package com.ddang.ddang.chat.domain.repository; + +import com.ddang.ddang.chat.domain.ReadMessageLog; + +import java.util.List; +import java.util.Optional; + +public interface ReadMessageLogRepository { + + Optional findBy(final Long readerId, final Long chatRoomId); + + List saveAll(List readMessageLogs); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java new file mode 100644 index 000000000..8ceceeb93 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface JpaReadMessageLogRepository extends JpaRepository { + + @Query(""" + SELECT rml + FROM ReadMessageLog rml + WHERE rml.chatRoom.id = :chatRoomId AND rml.reader.id = :readerId + """) + Optional findLastReadMessageByUserIdAndChatRoomId(final Long readerId, final Long chatRoomId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java index 07e7bc5f7..26734527e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java @@ -5,6 +5,7 @@ import com.ddang.ddang.chat.infrastructure.persistence.dto.QChatRoomAndMessageAndImageQueryProjectionDto; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -15,6 +16,7 @@ import static com.ddang.ddang.auction.domain.QAuction.auction; import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; import static com.ddang.ddang.chat.domain.QMessage.message; +import static com.ddang.ddang.chat.domain.QReadMessageLog.readMessageLog; import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage; import static java.util.Comparator.comparing; @@ -26,8 +28,12 @@ public class QuerydslChatRoomAndMessageAndImageRepository { public List findAllChatRoomInfoByUserIdOrderByLastMessage(final Long userId) { final List unsortedDtos = - queryFactory.select(new QChatRoomAndMessageAndImageQueryProjectionDto(chatRoom, message, auctionImage)) - .from(chatRoom) + queryFactory.select(new QChatRoomAndMessageAndImageQueryProjectionDto( + chatRoom, + message, + auctionImage, + countUnreadMessages(userId) + )).from(chatRoom) .leftJoin(chatRoom.buyer).fetchJoin() .leftJoin(chatRoom.auction, auction).fetchJoin() .leftJoin(auction.seller).fetchJoin() @@ -52,6 +58,21 @@ public List findAllChatRoomInfoByUserIdOrderByLas return sortByLastMessageIdDesc(unsortedDtos); } + private static JPQLQuery countUnreadMessages(final Long userId) { + return JPAExpressions.select(message.count()) + .from(message) + .where( + message.chatRoom.id.eq(chatRoom.id), + message.writer.id.ne(userId), + message.id.gt( + JPAExpressions + .select(readMessageLog.lastReadMessageId) + .from(readMessageLog) + .where(readMessageLog.reader.id.eq(userId)) + ) + ); + } + private List sortByLastMessageIdDesc( final List unsortedDtos ) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java new file mode 100644 index 000000000..dceecf3ed --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class ReadMessageLogRepositoryImpl implements ReadMessageLogRepository { + + private final JpaReadMessageLogRepository jpaReadMessageLogRepository; + + @Override + public Optional findBy(final Long readerId, final Long chatRoomId) { + return jpaReadMessageLogRepository.findLastReadMessageByUserIdAndChatRoomId(readerId, chatRoomId); + } + + @Override + public List saveAll(final List readMessageLogs) { + return jpaReadMessageLogRepository.saveAll(readMessageLogs); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java index c4369d8f3..f4a2b49a3 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java @@ -6,7 +6,12 @@ import com.ddang.ddang.image.domain.AuctionImage; import com.querydsl.core.annotations.QueryProjection; -public record ChatRoomAndMessageAndImageQueryProjectionDto(ChatRoom chatRoom, Message message, AuctionImage auctionImage) { +public record ChatRoomAndMessageAndImageQueryProjectionDto( + ChatRoom chatRoom, + Message message, + AuctionImage auctionImage, + Long unreadMessage +) { @QueryProjection public ChatRoomAndMessageAndImageQueryProjectionDto { @@ -16,7 +21,8 @@ public ChatRoomAndMessageAndImageDto toSortedDto() { return new ChatRoomAndMessageAndImageDto( this.chatRoom, this.message, - this.auctionImage + this.auctionImage, + this.unreadMessage ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java index 7aa99edc0..4c35179ea 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java @@ -7,6 +7,7 @@ public record ReadChatRoomWithLastMessageResponse( ReadChatPartnerResponse chatPartner, ReadAuctionInChatRoomResponse auction, ReadLastMessageResponse lastMessage, + Long unreadMessageCount, boolean isChatAvailable ) { @@ -16,6 +17,7 @@ public static ReadChatRoomWithLastMessageResponse from(final ReadChatRoomWithLas ReadChatPartnerResponse.from(dto.partnerDto()), ReadAuctionInChatRoomResponse.from(dto.auctionDto()), ReadLastMessageResponse.from(dto.lastMessageDto()), + dto.unreadMessageCount(), dto.isChatAvailable() ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index e3c7b2ea0..5831faf10 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -16,6 +16,7 @@ import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; import com.ddang.ddang.exception.dto.ExceptionResponse; @@ -111,6 +112,14 @@ public ResponseEntity handleMessageNotFoundException(final Me .body(new ExceptionResponse(ex.getMessage())); } + @ExceptionHandler(ReadMessageLogNotFoundException.class) + public ResponseEntity handleReadMessageNotFoundException(final ReadMessageLogNotFoundException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + @ExceptionHandler(UnableToChatException.class) public ResponseEntity handleUnableToChatException(final UnableToChatException ex) { logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java index 6f1b7fdfc..bf909cb60 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java @@ -10,8 +10,6 @@ public interface UserRepository { Optional findById(final Long id); - boolean existsById(final Long id); - Optional findByOauthId(final String oauthId); boolean existsByIdAndDeletedIsTrue(final Long id); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java index 5a5e3fac6..1cff56988 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java @@ -23,11 +23,6 @@ public Optional findById(final Long id) { return jpaUserRepository.findById(id); } - @Override - public boolean existsById(final Long id) { - return jpaUserRepository.existsById(id); - } - @Override public Optional findByOauthId(final String oauthId) { return jpaUserRepository.findByOauthId(oauthId); diff --git a/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql b/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql new file mode 100644 index 000000000..0cc6063d2 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql @@ -0,0 +1,10 @@ +create table read_message_log ( + id bigint not null auto_increment, + chat_room_id bigint, + reader_id bigint, + last_read_message_id bigint, + primary key (id) +); + +alter table read_message_log add constraint fk_read_message_log_chat_room foreign key (chat_room_id) references chat_room (id); +alter table read_message_log add constraint fk_read_message_reader foreign key (reader_id) references users (id); diff --git a/backend/ddang/src/main/resources/static/docs/docs.html b/backend/ddang/src/main/resources/static/docs/docs.html index 8bef9d493..4cf03fadb 100644 --- a/backend/ddang/src/main/resources/static/docs/docs.html +++ b/backend/ddang/src/main/resources/static/docs/docs.html @@ -516,9 +516,9 @@

땅땅땅 API 문서

  • 채팅방 신고 등록
  • 채팅방 신고 조회
  • 질문 신고 등록
  • -
  • 질문 신고 조회
  • +
  • 채팅방 신고 조회
  • 답변 신고 등록
  • -
  • 답변 신고 조회
  • +
  • 채팅방 신고 조회
  • 디바이스 토큰 API @@ -531,7 +531,7 @@

    땅땅땅 API 문서

  • 사용자 평가 등록
  • 지정한 평가 아이디에 해당하는 평가 조회
  • 지정한 사용자가 받은 평가 목록 조회
  • -
  • 사용자가 경매 거래 상대에게 작성한 평가를 조회한다
  • +
  • 사용자가_경매_거래_상대에게_작성한_평가를_조회한다
  • @@ -914,6 +914,25 @@

    요청

    + ++++ + + + + + + + + + + + + +
    Table 2. /oauth2/withdrawal/{oauth2Type}
    ParameterDescription

    oauth2Type

    소셜 로그인을 할 서비스 선택(kakao로 고정)

    +@@ -1275,7 +1294,7 @@

    요청

    - +@@ -1354,7 +1373,7 @@

    요청

    Table 2. /regions/{firstId}Table 3. /regions/{firstId}
    - +@@ -1450,7 +1469,7 @@

    요청

    Content-Disposition: form-data; name=request; filename=request Content-Type: application/json -{"title":"제목","description":"내용","bidUnit":1000,"startPrice":1000,"closingTime":"2023-10-22T15:18:31.556019","subCategoryId":2,"thirdRegionIds":[3]} +{"title":"제목","description":"내용","bidUnit":1000,"startPrice":1000,"closingTime":"2023-11-03T16:59:34.062594","subCategoryId":2,"thirdRegionIds":[3]} --6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm-- @@ -1999,7 +2018,7 @@

    요청

    Table 3. /regions/{firstId}/{secondId}Table 4. /regions/{firstId}/{secondId}
    - +@@ -2057,8 +2076,8 @@

    응답

    "lastBidPrice" : null, "status" : "UNBIDDEN", "bidUnit" : 1000, - "registerTime" : "2023-10-19T15:18:31", - "closingTime" : "2023-10-19T15:18:31", + "registerTime" : "2023-10-31T16:59:34", + "closingTime" : "2023-10-31T16:59:34", "directRegions" : [ { "first" : "서울특별시", "second" : "강서구", @@ -2251,7 +2270,7 @@

    요청

    Table 4. /auctions/{auctionId}Table 5. /auctions/{auctionId}
    - +@@ -2402,7 +2421,7 @@

    요청

    Table 5. /auctions/{auctionId}Table 6. /auctions/{auctionId}
    - +@@ -2530,7 +2549,7 @@

    요청

    Table 6. /questions/{questionId}Table 7. /questions/{questionId}
    - +@@ -2586,7 +2605,7 @@

    응답

    "name" : "질문자", "image" : "http://localhost:8080/users/images/1" }, - "createdTime" : "2023-10-19T15:18:31", + "createdTime" : "2023-10-31T16:59:34", "content" : "질문1", "isQuestioner" : false }, @@ -2597,7 +2616,7 @@

    응답

    "name" : "판매자", "image" : "http://localhost:8080/users/images/2" }, - "createdTime" : "2023-10-19T15:18:31", + "createdTime" : "2023-10-31T16:59:34", "content" : "답변1" } }, { @@ -2608,7 +2627,7 @@

    응답

    "name" : "질문자", "image" : "http://localhost:8080/users/images/1" }, - "createdTime" : "2023-10-19T15:18:31", + "createdTime" : "2023-10-31T16:59:34", "content" : "질문2", "isQuestioner" : false }, @@ -2619,7 +2638,7 @@

    응답

    "name" : "판매자", "image" : "http://localhost:8080/users/images/2" }, - "createdTime" : "2023-10-19T15:18:31", + "createdTime" : "2023-10-31T16:59:34", "content" : "답변1" } } ] @@ -2816,7 +2835,7 @@

    입찰 조회

    요청

    -
    GET /bids/1 HTTP/1.1
    +
    GET /bids/-999 HTTP/1.1
     Content-Type: application/json
    @@ -2832,12 +2851,12 @@

    응답

    "name" : "사용자1", "profileImage" : "http://localhost:8080/users/images/1", "price" : 10000, - "bidTime" : "2023-10-19T15:18:36" + "bidTime" : "2023-10-31T16:59:40" }, { "name" : "사용자2", "profileImage" : "http://localhost:8080/users/images/2", "price" : 12000, - "bidTime" : "2023-10-19T15:18:36" + "bidTime" : "2023-10-31T16:59:40" } ]
    @@ -3031,7 +3050,7 @@

    응답

    "price" : 10000 }, "lastMessage" : { - "createdAt" : "2023-10-19T15:18:38", + "createdAt" : "2023-10-31T16:59:45", "contents" : "메시지1" }, "isChatAvailable" : true @@ -3049,7 +3068,7 @@

    응답

    "price" : 20000 }, "lastMessage" : { - "createdAt" : "2023-10-19T15:18:38", + "createdAt" : "2023-10-31T16:59:45", "contents" : "메시지2" }, "isChatAvailable" : true @@ -3161,7 +3180,7 @@

    요청

    Table 7. /questions/answers/{answerId}Table 8. /questions/answers/{answerId}
    - +@@ -3312,7 +3331,7 @@

    요청

    Table 8. /chattings/{chatRoomId}Table 9. /chattings/{chatRoomId}
    - +@@ -3423,7 +3442,7 @@

    요청

    Table 9. /chattings/{chatRoomId}/messagesTable 10. /chattings/{chatRoomId}/messages
    - +@@ -3487,7 +3506,7 @@

    응답

    [ { "id" : 1, - "createdAt" : "2023-10-19T15:18:38", + "createdAt" : "2023-10-31T16:59:44", "isMyMessage" : true, "contents" : "메시지내용" } ] @@ -3637,7 +3656,7 @@

    응답

    "id" : 2, "name" : "회원1" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3649,7 +3668,7 @@

    응답

    "id" : 3, "name" : "회원2" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3661,7 +3680,7 @@

    응답

    "id" : 4, "name" : "회원3" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3825,7 +3844,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3836,7 +3855,7 @@

    응답

    "id" : 3, "name" : "구매자2" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3847,7 +3866,7 @@

    응답

    "id" : 3, "name" : "구매자2" }, - "createdTime" : "2023-10-19T15:18:45", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3987,7 +4006,7 @@

    응답

    -

    질문 신고 조회

    +

    채팅방 신고 조회

    요청

    @@ -4011,7 +4030,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 1 }, @@ -4022,7 +4041,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 2 }, @@ -4033,7 +4052,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 3 }, @@ -4173,7 +4192,7 @@

    응답

    -

    답변 신고 조회

    +

    채팅방 신고 조회

    요청

    @@ -4197,7 +4216,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 1 }, @@ -4208,7 +4227,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 2 }, @@ -4219,7 +4238,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T15:18:46", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 3 }, @@ -4449,7 +4468,7 @@

    요청

    Table 10. /chattings/{chatRoomId}/messagesTable 11. /chattings/{chatRoomId}/messages
    - +@@ -4520,7 +4539,7 @@

    요청

    Table 11. /reviews/{reviewId}Table 12. /reviews/{reviewId}
    - +@@ -4555,7 +4574,7 @@

    응답

    }, "content" : "친절하다.", "score" : 5.0, - "createdTime" : "2023-10-19T15:18:46" + "createdTime" : "2023-10-31T16:59:56" }, { "id" : 2, "writer" : { @@ -4565,7 +4584,7 @@

    응답

    }, "content" : "친절하다.", "score" : 5.0, - "createdTime" : "2023-10-19T15:18:46" + "createdTime" : "2023-10-31T16:59:56" } ] @@ -4628,7 +4647,7 @@

    응답

    Table 12. /reviews/users/{userId}Table 13. /reviews/users/{userId}
    - +@@ -4723,7 +4742,7 @@

    응답

    @@ -4734,4 +4753,4 @@

    응답

    } - \ No newline at end of file + diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java index de0cccb21..eeb42b1fb 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java @@ -5,10 +5,13 @@ import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.fixture.ChatRoomServiceFixture; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.user.application.exception.UserNotFoundException; import org.assertj.core.api.SoftAssertions; @@ -16,6 +19,8 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; import java.util.List; @@ -23,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @IsolateDatabase +@RecordApplicationEvents @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class ChatRoomServiceTest extends ChatRoomServiceFixture { @@ -30,6 +36,15 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { @Autowired ChatRoomService chatRoomService; + @Autowired + ReadMessageLogRepository readMessageLogRepository; + + @Autowired + MessageRepository messageRepository; + + @Autowired + ApplicationEvents events; + @Test void 채팅방을_생성한다() { // when @@ -39,6 +54,16 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { assertThat(actual).isPositive(); } + @Test + void 채팅방_생성시_메시지_로그_생성_이벤트가_호출된다() { + // when + chatRoomService.create(구매자.getId(), 채팅방_생성을_위한_DTO); + final long actual = events.stream(CreateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + @Test void 채팅방_생성시_요청한_사용자_정보를_찾을_수_없다면_예외가_발생한다() { // when & then @@ -96,8 +121,21 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).hasSize(2); - softAssertions.assertThat(actual.get(0)).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보); - softAssertions.assertThat(actual.get(1)).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.id()); + softAssertions.assertThat(actual.get(0).auctionDto()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.auctionDto()); + softAssertions.assertThat(actual.get(0).partnerDto()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.partnerDto()); + softAssertions.assertThat(actual.get(0).lastMessageDto()) + .isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.lastMessageDto()); + softAssertions.assertThat(actual.get(0).isChatAvailable()) + .isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.isChatAvailable()); + + softAssertions.assertThat(actual.get(1).id()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.id()); + softAssertions.assertThat(actual.get(1).auctionDto()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.auctionDto()); + softAssertions.assertThat(actual.get(1).partnerDto()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.partnerDto()); + softAssertions.assertThat(actual.get(1).lastMessageDto()) + .isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.lastMessageDto()); + softAssertions.assertThat(actual.get(1).isChatAvailable()) + .isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.isChatAvailable()); }); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java new file mode 100644 index 000000000..5e0041468 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java @@ -0,0 +1,76 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.application.fixture.LastReadMessageLogEventListenerFixture; +import com.ddang.ddang.configuration.IsolateDatabase; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.verify; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class LastReadMessageLogEventListenerTest extends LastReadMessageLogEventListenerFixture { + + @Autowired + ApplicationEvents events; + + @Autowired + LastReadMessageLogEventListener lastReadMessageLogEventListener; + + @MockBean + LastReadMessageLogService lastReadMessageLogService; + + @Test + void 이벤트가_호출되면_메시지_로그를_저장한다() { + // given + willDoNothing().given(lastReadMessageLogService).create(any()); + + // when + lastReadMessageLogEventListener.create(생성용_메시지_조회_로그); + + // then + verify(lastReadMessageLogService).create(any()); + } + + @Test + void 메시지_로그_저장에_실패한_경우_예외가_발생하지_않는다() { + // given + willThrow(IllegalArgumentException.class).given(lastReadMessageLogService).create(any()); + + // when & then + assertDoesNotThrow(() -> lastReadMessageLogEventListener.create(생성용_메시지_조회_로그)); + } + + @Test + void 이벤트가_호출되면_메시지_로그를_업데이트한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + + // when + lastReadMessageLogEventListener.update(업데이트용_메시지_조회_로그); + + // then + verify(lastReadMessageLogService).update(any()); + } + + @Test + void 메시지_로그_업데이트에_실패한_경우_예외가_발생하지_않는다() { + // given + willThrow(ReadMessageLogNotFoundException.class).given(lastReadMessageLogService).update(any()); + + // when & then + assertDoesNotThrow(() -> lastReadMessageLogEventListener.update(업데이트용_메시지_조회_로그)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java new file mode 100644 index 000000000..5512fabe3 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.application.fixture.LastReadMessageLogServiceFixture; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.configuration.IsolateDatabase; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.RecordApplicationEvents; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class LastReadMessageLogServiceTest extends LastReadMessageLogServiceFixture { + + @Autowired + LastReadMessageLogService lastReadMessageLogService; + + @Autowired + ReadMessageLogRepository readMessageLogRepository; + + @Test + void 메시지_로그를_생성한다() { + // given & when + lastReadMessageLogService.create(메시지_로그_생성용_이벤트); + final Optional actual_판매자 = readMessageLogRepository.findBy(메시지_로그_생성용_발신자_겸_판매자.getId(), 메시지_로그_생성용_채팅방.getId()); + final Optional actual_구매자 = readMessageLogRepository.findBy(메시지_로그_생성용_입찰자_구매자.getId(), 메시지_로그_생성용_채팅방.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_판매자).isPresent(); + softAssertions.assertThat(actual_구매자).isPresent(); + }); + } + + @Test + void 메시지_로그를_업데이트한다() { + // given & when + lastReadMessageLogService.update(메시지_로그_업데이트용_이벤트); + + final Optional actual_발신자 = readMessageLogRepository.findBy( + 메시지_로그_업데이트용_발신자_겸_판매자.getId(), + 메시지_로그_업데이트용_채팅방.getId() + ); + final Optional actual_입찰자 = readMessageLogRepository.findBy( + 메시지_로그_업데이트용_입찰자.getId(), + 메시지_로그_업데이트용_채팅방.getId() + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_발신자).isPresent(); + softAssertions.assertThat(actual_발신자.get().getLastReadMessageId()) + .isEqualTo(메시지_로그_업데이트용_마지막_조회_메시지.getId()); + softAssertions.assertThat(actual_입찰자.get().getLastReadMessageId()) + .isNotEqualTo(메시지_로그_업데이트용_마지막_조회_메시지.getId()); + }); + } + + @Test + void 메시지_로그_업데이트시_메시지_조회_로그가_존재하지_않으면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> lastReadMessageLogService.update(유효하지_않는_메시지_조회_로그)) + .isInstanceOf(ReadMessageLogNotFoundException.class); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java index a65cadb92..7794679c5 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java @@ -2,9 +2,11 @@ import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.fixture.MessageServiceFixture; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.notification.application.NotificationService; import com.ddang.ddang.notification.application.dto.CreateNotificationDto; @@ -25,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; @IsolateDatabase @RecordApplicationEvents @@ -35,9 +38,15 @@ class MessageServiceTest extends MessageServiceFixture { @Autowired MessageService messageService; + @Autowired + ReadMessageLogRepository readMessageLogRepository; + @MockBean NotificationService notificationService; + @MockBean + LastReadMessageLogService lastReadMessageLogService; + @Autowired ApplicationEvents events; @@ -101,6 +110,9 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 마지막_조회_메시지가_없는_경우_모든_메시지를_조회한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + // when final List actual = messageService.readAllByLastMessageId(마지막_조회_메시지_아이디가_없는_메시지_조회용_request); @@ -110,6 +122,9 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 첫_번째_메시지_이후에_생성된_모든_메시지를_조회한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + // when final List actual = messageService.readAllByLastMessageId(두_번째_메시지부터_모든_메시지_조회용_request); @@ -126,6 +141,16 @@ class MessageServiceTest extends MessageServiceFixture { assertThat(readMessageDtos).isEmpty(); } + @Test + void 메시지를_조회할_경우_마지막으로_읽은_메시지_업데이트_이벤트를_호출한다() { + // when + messageService.readAllByLastMessageId(조회한_마지막_메시지가_5인_메시지_조회용_request); + final long actual = events.stream(UpdateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + @Test void 잘못된_사용자가_메시지를_조회할_경우_예외가_발생한다() { // when & then diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java index 234913411..e53afed2b 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java @@ -59,6 +59,8 @@ public class ChatRoomServiceFixture { protected User 엔초; protected User 제이미; protected User 지토; + protected User 채팅방을_생성하는_메리; + protected User 메리_경매_낙찰자_지토; protected User 경매에_참여한_적_없는_사용자; protected Auction 채팅방이_없는_경매; protected Auction 판매자_엔초_구매자_지토_경매; @@ -78,6 +80,7 @@ public class ChatRoomServiceFixture { protected CreateChatRoomDto 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO; protected CreateChatRoomDto 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO; protected CreateChatRoomDto 엔초_지토_채팅방_생성을_위한_DTO; + protected CreateChatRoomDto 메리가_생성하려는_채팅방; protected ReadParticipatingChatRoomDto 엔초가_조회한_엔초_지토_채팅방_정보_조회_결과; protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_가능; protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_불가능; @@ -120,32 +123,46 @@ void setUp() { .name("엔초") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12346") + .oauthId("12347") .build(); 제이미 = User.builder() .name("제이미") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12347") + .oauthId("12348") .build(); 지토 = User.builder() .name("지토") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12348") + .oauthId("12349") .build(); 경매에_참여한_적_없는_사용자 = User.builder() .name("외부인") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12349") + .oauthId("12340") .build(); + 채팅방을_생성하는_메리 = User.builder() + .name("채팅방을_생성하는_메리") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("09876") + .build(); + 메리_경매_낙찰자_지토 = User.builder() + .name("메리_경매_낙찰자_지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("10293") + .build(); userRepository.save(판매자); userRepository.save(구매자); userRepository.save(엔초); userRepository.save(제이미); userRepository.save(지토); userRepository.save(경매에_참여한_적_없는_사용자); + userRepository.save(채팅방을_생성하는_메리); + userRepository.save(메리_경매_낙찰자_지토); 채팅방이_없는_경매 = Auction.builder() .seller(판매자) @@ -192,25 +209,39 @@ void setUp() { .bidUnit(new BidUnit(1_000)) .closingTime(LocalDateTime.now()) .build(); + final Auction 판매자_메리_구매자_지토_경매 = Auction.builder() + .seller(채팅방을_생성하는_메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); 채팅방이_없는_경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); 판매자_엔초_구매자_지토_경매.addAuctionImages(List.of(엔초의_경매_대표_이미지, 엔초의_대표_이미지가_아닌_경매_이미지)); 판매자_제이미_구매자_엔초_경매.addAuctionImages(List.of(제이미의_경매_대표_이미지, 제이미의_대표_이미지가_아닌_경매_이미지)); + 판매자_메리_구매자_지토_경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); auctionRepository.save(채팅방이_없는_경매); auctionRepository.save(종료되지_않은_경매); auctionRepository.save(낙찰자가_없는_경매); auctionRepository.save(판매자_엔초_구매자_지토_경매); auctionRepository.save(판매자_제이미_구매자_엔초_경매); + auctionRepository.save(판매자_메리_구매자_지토_경매); final Bid 채팅방_없는_경매_입찰 = new Bid(채팅방이_없는_경매, 구매자, new BidPrice(15_000)); final Bid 지토가_엔초_경매에_입찰 = new Bid(판매자_엔초_구매자_지토_경매, 지토, new BidPrice(15_000)); final Bid 엔초가_제이미_경매에_입찰 = new Bid(판매자_제이미_구매자_엔초_경매, 엔초, new BidPrice(15_000)); + final Bid 지토가_메리_경매에_입찰 = new Bid(판매자_메리_구매자_지토_경매, 메리_경매_낙찰자_지토, new BidPrice(15_000)); bidRepository.save(채팅방_없는_경매_입찰); bidRepository.save(지토가_엔초_경매에_입찰); bidRepository.save(엔초가_제이미_경매에_입찰); + bidRepository.save(지토가_메리_경매에_입찰); 채팅방이_없는_경매.updateLastBid(채팅방_없는_경매_입찰); 판매자_엔초_구매자_지토_경매.updateLastBid(지토가_엔초_경매에_입찰); 판매자_제이미_구매자_엔초_경매.updateLastBid(엔초가_제이미_경매에_입찰); + 판매자_메리_구매자_지토_경매.updateLastBid(지토가_메리_경매에_입찰); 엔초_지토_채팅방 = new ChatRoom(판매자_엔초_구매자_지토_경매, 지토); 제이미_엔초_채팅방 = new ChatRoom(판매자_제이미_구매자_엔초_경매, 엔초); @@ -239,6 +270,7 @@ void setUp() { 경매에_참여한_적_없는_사용자_정보 = new AuthenticationUserInfo(경매에_참여한_적_없는_사용자.getId()); 존재하지_않는_사용자_정보 = new AuthenticationUserInfo(존재하지_않는_사용자_아이디); 채팅방_생성을_위한_DTO = new CreateChatRoomDto(채팅방이_없는_경매.getId()); + 메리가_생성하려는_채팅방 = new CreateChatRoomDto(판매자_메리_구매자_지토_경매.getId()); 경매_정보가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(존재하지_않는_경매_아이디); 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(종료되지_않은_경매.getId()); 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(낙찰자가_없는_경매.getId()); @@ -253,6 +285,7 @@ void setUp() { ReadAuctionInChatRoomDto.of(판매자_제이미_구매자_엔초_경매, 제이미의_경매_대표_이미지), ReadUserInChatRoomDto.from(제이미), ReadLastMessageDto.from(제이미가_엔초에게_2시에_보낸_쪽지), + 1L, true ); 엔초_채팅_목록의_엔초_지토_채팅방_정보 = new ReadChatRoomWithLastMessageDto( @@ -260,6 +293,7 @@ void setUp() { ReadAuctionInChatRoomDto.of(판매자_엔초_구매자_지토_경매, 엔초의_경매_대표_이미지), ReadUserInChatRoomDto.from(지토), ReadLastMessageDto.from(엔초가_지토에게_1시에_보낸_쪽지), + 1L, true ); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java new file mode 100644 index 000000000..dc0bd5c35 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java @@ -0,0 +1,63 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class LastReadMessageLogEventListenerFixture { + + protected CreateReadMessageLogEvent 생성용_메시지_조회_로그; + protected UpdateReadMessageLogEvent 업데이트용_메시지_조회_로그; + protected User 메시지_로그_생성용_발신자_겸_판매자; + protected User 메시지_로그_생성용_입찰자_구매자; + protected User 메시지_로그_업데이트용_발신자_겸_판매자; + protected User 메시지_로그_업데이트용_입찰자; + protected ChatRoom 메시지_로그_생성용_채팅방; + protected Auction 메시지_로그_생성용_경매; + + @BeforeEach + void setUp() { + 메시지_로그_생성용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_생성용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 메시지_로그_생성용_입찰자_구매자 = User.builder() + .name("메시지_로그_생성용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 메시지_로그_업데이트용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_업데이트용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 메시지_로그_업데이트용_입찰자 = User.builder() + .name("메시지_로그_업데이트용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + + 메시지_로그_생성용_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + + final Message 메시지 = Message.builder() + .chatRoom(메시지_로그_생성용_채팅방) + .writer(메시지_로그_생성용_발신자_겸_판매자) + .contents("메시지") + .build(); + + 업데이트용_메시지_조회_로그 = new UpdateReadMessageLogEvent(메시지_로그_생성용_발신자_겸_판매자, 메시지_로그_생성용_채팅방, 메시지); + 생성용_메시지_조회_로그 = new CreateReadMessageLogEvent(메시지_로그_생성용_채팅방); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java new file mode 100644 index 000000000..737969fd0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java @@ -0,0 +1,184 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.domain.repository.AuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.AuctionRepositoryImpl; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.QuerydslAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.BidRepositoryImpl; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ChatRoomRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ReadMessageLogRepositoryImpl; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.domain.repository.UserRepository; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.user.infrastructure.persistence.UserRepositoryImpl; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class LastReadMessageLogServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + private AuctionRepository auctionRepository; + + private UserRepository userRepository; + + private ChatRoomRepository chatRoomRepository; + + private BidRepositoryImpl bidRepository; + + private ReadMessageLogRepository readMessageLogRepository; + + protected CreateReadMessageLogEvent 메시지_로그_생성용_이벤트; + protected UpdateReadMessageLogEvent 메시지_로그_업데이트용_이벤트; + protected UpdateReadMessageLogEvent 유효하지_않는_메시지_조회_로그; + protected User 메시지_로그_생성용_발신자_겸_판매자; + protected User 메시지_로그_생성용_입찰자_구매자; + protected User 메시지_로그_업데이트용_발신자_겸_판매자; + protected User 메시지_로그_업데이트용_입찰자; + protected User 저장되지_않은_사용자; + protected Auction 메시지_로그_생성용_경매; + protected Message 메시지_로그_생성용_마지막_조회_메시지; + protected Message 메시지_로그_업데이트용_마지막_조회_메시지; + protected Auction 메시지_로그_업데이트용_경매; + protected ChatRoom 메시지_로그_생성용_채팅방; + protected ChatRoom 저장되지_않은_채팅방; + protected ChatRoom 메시지_로그_업데이트용_채팅방; + + @BeforeEach + void fixtureSetUp( + @Autowired final JPAQueryFactory jpaQueryFactory, + @Autowired final JpaAuctionRepository jpaAuctionRepository, + @Autowired final JpaUserRepository jpaUserRepository, + @Autowired final JpaChatRoomRepository jpaChatRoomRepository, + @Autowired final JpaBidRepository jpaBidRepository, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository + ) { + auctionRepository = new AuctionRepositoryImpl(jpaAuctionRepository, new QuerydslAuctionRepository(jpaQueryFactory)); + userRepository = new UserRepositoryImpl(jpaUserRepository); + chatRoomRepository = new ChatRoomRepositoryImpl(jpaChatRoomRepository); + bidRepository = new BidRepositoryImpl(jpaBidRepository); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + + final Category 전자기기 = new Category("전자기기"); + final Category 전자기기_하위_노트북 = new Category("노트북"); + 전자기기.addSubCategory(전자기기_하위_노트북); + categoryRepository.save(전자기기); + + 메시지_로그_생성용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_생성용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 메시지_로그_생성용_입찰자_구매자 = User.builder() + .name("메시지_로그_생성용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 메시지_로그_업데이트용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_업데이트용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 메시지_로그_업데이트용_입찰자 = User.builder() + .name("메시지_로그_업데이트용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 저장되지_않은_사용자 = User.builder() + .name("저장되지_않은_사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + userRepository.save(메시지_로그_생성용_발신자_겸_판매자); + userRepository.save(메시지_로그_생성용_입찰자_구매자); + userRepository.save(메시지_로그_업데이트용_발신자_겸_판매자); + userRepository.save(메시지_로그_업데이트용_입찰자); + + 메시지_로그_생성용_경매 = Auction.builder() + .title("경매") + .seller(메시지_로그_생성용_발신자_겸_판매자) + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + 메시지_로그_업데이트용_경매 = Auction.builder() + .seller(메시지_로그_업데이트용_발신자_겸_판매자) + .title("메시지_로그_업데이트용_경매") + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(메시지_로그_생성용_경매); + auctionRepository.save(메시지_로그_업데이트용_경매); + + final Bid 메시지_로그_생성용_입찰 = new Bid(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자, new BidPrice(15_000)); + final Bid 메시지_로그_업데이트용_입찰 = new Bid(메시지_로그_업데이트용_경매, 메시지_로그_업데이트용_입찰자, new BidPrice(15_000)); + bidRepository.save(메시지_로그_생성용_입찰); + bidRepository.save(메시지_로그_업데이트용_입찰); + 메시지_로그_생성용_경매.updateLastBid(메시지_로그_생성용_입찰); + 메시지_로그_업데이트용_경매.updateLastBid(메시지_로그_업데이트용_입찰); + + 메시지_로그_생성용_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + 저장되지_않은_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + 메시지_로그_업데이트용_채팅방 = new ChatRoom(메시지_로그_업데이트용_경매, 메시지_로그_업데이트용_발신자_겸_판매자); + chatRoomRepository.save(메시지_로그_생성용_채팅방); + chatRoomRepository.save(메시지_로그_업데이트용_채팅방); + + 메시지_로그_생성용_이벤트 = new CreateReadMessageLogEvent(메시지_로그_생성용_채팅방); + + 메시지_로그_생성용_마지막_조회_메시지 = Message.builder() + .writer(메시지_로그_생성용_발신자_겸_판매자) + .receiver(메시지_로그_생성용_입찰자_구매자) + .contents("메시지") + .build(); + 메시지_로그_업데이트용_마지막_조회_메시지 = Message.builder() + .writer(메시지_로그_업데이트용_발신자_겸_판매자) + .receiver(메시지_로그_업데이트용_입찰자) + .contents("메시지") + .build(); + final Message 저장되지_않은_메시지 = Message.builder() + .writer(저장되지_않은_사용자) + .contents("저장되지 않은 메시지") + .build(); + + final ReadMessageLog 메시지_로그_업데이트용_로그_판매자 = new ReadMessageLog(메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_발신자_겸_판매자); + final ReadMessageLog 메시지_로그_업데이트용_로그_구매자 = new ReadMessageLog(메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_입찰자); + readMessageLogRepository.saveAll(List.of(메시지_로그_업데이트용_로그_판매자, 메시지_로그_업데이트용_로그_구매자)); + + 메시지_로그_업데이트용_이벤트 = new UpdateReadMessageLogEvent(메시지_로그_업데이트용_발신자_겸_판매자, 메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_마지막_조회_메시지); + + 유효하지_않는_메시지_조회_로그 = new UpdateReadMessageLogEvent(저장되지_않은_사용자, 저장되지_않은_채팅방, 저장되지_않은_메시지); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java index 892695e39..01765bf70 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java @@ -49,9 +49,13 @@ public class MessageServiceFixture { protected ReadMessageRequest 마지막_조회_메시지_아이디가_없는_메시지_조회용_request; protected ReadMessageRequest 두_번째_메시지부터_모든_메시지_조회용_request; protected ReadMessageRequest 조회할_메시지가_더이상_없는_메시지_조회용_request; + protected ReadMessageRequest 조회한_마지막_메시지가_5인_메시지_조회용_request; protected ReadMessageRequest 유효하지_않은_사용자의_메시지_조회용_request; protected ReadMessageRequest 유효하지_않은_채팅방의_메시지_조회용_request; protected ReadMessageRequest 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request; + protected User 발신자; + protected ChatRoom 메시지가_5개인_채팅방; + protected Message 메시지가_5개인_채팅방_메시지의_마지막_메시지; protected String 이미지_절대_경로 = "/imageUrl"; protected int 메시지_총_개수 = 10; @@ -72,12 +76,12 @@ void setUp() { .build(); auctionRepository.save(경매); - final User 발신자 = User.builder() - .name("발신자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + 발신자 = User.builder() + .name("발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); final User 수신자 = User.builder() .name("수신자") .profileImage(new ProfileImage("upload.png", "store.png")) @@ -97,9 +101,11 @@ void setUp() { final ChatRoom 채팅방 = new ChatRoom(경매, 발신자); final ChatRoom 탈퇴한_사용자와의_채팅방 = new ChatRoom(경매, 탈퇴한_사용자); + 메시지가_5개인_채팅방 = new ChatRoom(경매, 발신자); chatRoomRepository.save(채팅방); chatRoomRepository.save(탈퇴한_사용자와의_채팅방); + chatRoomRepository.save(메시지가_5개인_채팅방); 메시지_생성_DTO = new CreateMessageDto( 채팅방.getId(), @@ -144,6 +150,19 @@ void setUp() { messageRepository.save(메시지); } + final List 메시지가_5개인_채팅방_메시지들 = new ArrayList<>(); + for (int count = 0; count < 5; count++) { + final Message 메시지 = Message.builder() + .writer(발신자) + .receiver(수신자) + .chatRoom(메시지가_5개인_채팅방) + .contents("메시지 내용") + .build(); + 메시지가_5개인_채팅방_메시지들.add(메시지); + messageRepository.save(메시지); + } + 메시지가_5개인_채팅방_메시지의_마지막_메시지 = 메시지가_5개인_채팅방_메시지들.get(4); + 마지막_조회_메시지_아이디가_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), null); 두_번째_메시지부터_모든_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(0).getId()); 조회할_메시지가_더이상_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(메시지_총_개수 - 1) @@ -151,5 +170,6 @@ void setUp() { 유효하지_않은_사용자의_메시지_조회용_request = new ReadMessageRequest(-999L, 채팅방.getId(), null); 유효하지_않은_채팅방의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), -999L, null); 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), -999L); + 조회한_마지막_메시지가_5인_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 메시지가_5개인_채팅방.getId(), null); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java index 0cb97b3e3..c12aa23f4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java @@ -4,8 +4,6 @@ import com.ddang.ddang.chat.infrastructure.persistence.fixture.JpaMessageRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java index 694cb6999..fc55b97c7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java @@ -1,6 +1,9 @@ package com.ddang.ddang.chat.infrastructure.persistence; +import com.ddang.ddang.chat.domain.ReadMessageLog; import com.ddang.ddang.chat.domain.dto.ChatRoomAndMessageAndImageDto; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslChatRoomAndMessageAndImageRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; @@ -24,9 +27,41 @@ class QuerydslChatRoomAndMessageAndImageRepositoryTest extends QuerydslChatRoomA QuerydslChatRoomAndMessageAndImageRepository querydslChatRoomAndMessageAndImageRepository; + ReadMessageLogRepository readMessageLogRepository; + + MessageRepository messageRepository; + @BeforeEach - void setUp(@Autowired final JPAQueryFactory queryFactory) { + void setUp( + @Autowired final JPAQueryFactory queryFactory, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository, + @Autowired final JpaMessageRepository jpaMessageRepository + ) { querydslChatRoomAndMessageAndImageRepository = new QuerydslChatRoomAndMessageAndImageRepository(queryFactory); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + messageRepository = new MessageRepositoryImpl(jpaMessageRepository, new QuerydslMessageRepository(queryFactory)); + } + + @Test + void 사용자가_읽지_않은_메시지_개수를_반환한다() { + // given + readMessageLogRepository.saveAll(List.of(new ReadMessageLog(메리_엔초_채팅방, 메리), new ReadMessageLog(메리_엔초_채팅방, 엔초))); + + // when + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지1); + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지2); + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지3); + messageRepository.save(엔초가_메리에게_3시에_보낸_쪽지); + final List actual_엔초 = querydslChatRoomAndMessageAndImageRepository + .findAllChatRoomInfoByUserIdOrderByLastMessage(엔초.getId()); + final List actual_메리 = querydslChatRoomAndMessageAndImageRepository + .findAllChatRoomInfoByUserIdOrderByLastMessage(메리.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_엔초.get(0).unreadMessageCount()).isEqualTo(3); + softAssertions.assertThat(actual_메리.get(0).unreadMessageCount()).isEqualTo(1); + }); } @Test @@ -37,16 +72,13 @@ void setUp(@Autowired final JPAQueryFactory queryFactory) { // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual).hasSize(2); softAssertions.assertThat(actual.get(0).chatRoom()).isEqualTo(엔초_지토_채팅방); softAssertions.assertThat(actual.get(0).message()).isEqualTo(엔초가_지토에게_5시에_보낸_쪽지); softAssertions.assertThat(actual.get(0).thumbnailImage()).isEqualTo(엔초의_경매_대표_이미지); softAssertions.assertThat(actual.get(1).chatRoom()).isEqualTo(제이미_엔초_채팅방); softAssertions.assertThat(actual.get(1).message()).isEqualTo(제이미가_엔초에게_4시에_보낸_쪽지); softAssertions.assertThat(actual.get(1).thumbnailImage()).isEqualTo(제이미의_경매_대표_이미지); - softAssertions.assertThat(actual.get(2).chatRoom()).isEqualTo(메리_엔초_채팅방); - softAssertions.assertThat(actual.get(2).message()).isEqualTo(메리가_엔초에게_3시에_보낸_쪽지); - softAssertions.assertThat(actual.get(2).thumbnailImage()).isEqualTo(메리의_경매_대표_이미지); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java new file mode 100644 index 000000000..ad254efd8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java @@ -0,0 +1,55 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.ReadMessageLogRepositoryFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ReadMessageLogRepositoryImplTest extends ReadMessageLogRepositoryFixture { + + ReadMessageLogRepository readMessageLogRepository; + + @BeforeEach + void setUp(@Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository) { + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + } + + @Test + void 마지막_읽은_메시지를_저장한다() { + // given + final ReadMessageLog actual = readMessageLogRepository.saveAll(List.of(다섯_번째_메시지까지_읽은_메시지_로그)).get(0); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 메시지_조회자_아이디와_채팅방_아이디에_해당하는_조회_메시지_로그를_반환한다() { + // given + final Optional actual = readMessageLogRepository.findBy(메리.getId(), 메리_엔초_채팅방.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + softAssertions.assertThat(actual.get().getLastReadMessageId()).isEqualTo(다섯_번째_메시지.getId()); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java index 49bdf7e18..357cbf052 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java @@ -58,15 +58,20 @@ public class QuerydslChatRoomAndMessageAndImageRepositoryFixture { private JpaMessageRepository messageRepository; protected User 엔초; + protected User 메리; protected AuctionImage 메리의_경매_대표_이미지; protected AuctionImage 엔초의_경매_대표_이미지; protected AuctionImage 제이미의_경매_대표_이미지; protected ChatRoom 메리_엔초_채팅방; protected ChatRoom 엔초_지토_채팅방; protected ChatRoom 제이미_엔초_채팅방; - protected Message 메리가_엔초에게_3시에_보낸_쪽지; protected Message 제이미가_엔초에게_4시에_보낸_쪽지; protected Message 엔초가_지토에게_5시에_보낸_쪽지; + protected Message 엔초가_지토에게_추가로_보낸_쪽지; + protected Message 메리가_엔초에게_3시에_보낸_쪽지1; + protected Message 메리가_엔초에게_3시에_보낸_쪽지2; + protected Message 메리가_엔초에게_3시에_보낸_쪽지3; + protected Message 엔초가_메리에게_3시에_보낸_쪽지; @BeforeEach void setUp() { @@ -80,24 +85,24 @@ void setUp() { .reliability(new Reliability(4.7d)) .oauthId("12346") .build(); - final User 메리 = User.builder() - .name("메리") - .profileImage(프로필_이미지) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + 메리 = User.builder() + .name("메리") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); final User 제이미 = User.builder() - .name("제이미") - .profileImage(프로필_이미지) - .reliability(new Reliability(4.7d)) - .oauthId("12347") - .build(); - final User 지토 = User.builder() - .name("지토") + .name("제이미") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12348") + .oauthId("12347") .build(); + final User 지토 = User.builder() + .name("지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); 메리의_경매_대표_이미지 = new AuctionImage("메리의_경매_대표_이미지.png", "메리의_경매_대표_이미지.png"); final AuctionImage 메리의_대표_이미지가_아닌_경매_이미지 = @@ -110,32 +115,32 @@ void setUp() { new AuctionImage("제이미의_대표 이미지가_아닌_경매_이미지.png", "제이미의_대표 이미지가_아닌_경매_이미지.png"); final Auction 메리의_경매 = Auction.builder() - .seller(메리) - .title("메리 맥북") - .description("메리 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Auction 엔초의_경매 = Auction.builder() - .seller(엔초) - .title("엔초 맥북") - .description("엔초 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(엔초) + .title("엔초 맥북") + .description("엔초 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Auction 제이미의_경매 = Auction.builder() - .seller(제이미) - .title("제이미 맥북") - .description("제이미 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(제이미) + .title("제이미 맥북") + .description("제이미 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Bid 엔초가_메리_경매에_입찰 = new Bid(메리의_경매, 엔초, new BidPrice(15_000)); final Bid 지토가_엔초_경매에_입찰 = new Bid(엔초의_경매, 지토, new BidPrice(15_000)); @@ -146,36 +151,65 @@ void setUp() { 제이미_엔초_채팅방 = new ChatRoom(제이미의_경매, 엔초); final Message 제이미가_엔초에게_1시에_보낸_쪽지 = Message.builder() - .chatRoom(제이미_엔초_채팅방) - .contents("제이미가 엔초에게 1시애 보낸 쪽지") - .writer(제이미) - .receiver(엔초) - .build(); + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 1시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); final Message 엔초가_지토에게_2시에_보낸_쪽지 = Message.builder() - .chatRoom(엔초_지토_채팅방) - .contents("엔초가 지토에게 2시애 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); - 메리가_엔초에게_3시에_보낸_쪽지 = Message.builder() - .chatRoom(메리_엔초_채팅방) - .contents("메리가 엔초에게 3시에 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 2시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); 제이미가_엔초에게_4시에_보낸_쪽지 = Message.builder() - .chatRoom(제이미_엔초_채팅방) - .contents("제이미가 엔초에게 4시애 보낸 쪽지") - .writer(제이미) - .receiver(엔초) - .build(); + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 4시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); 엔초가_지토에게_5시에_보낸_쪽지 = Message.builder() - .chatRoom(엔초_지토_채팅방) - .contents("엔초가 지토에게 5시애 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); - + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 5시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 엔초가_지토에게_추가로_보낸_쪽지 = Message.builder() + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 6시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지1 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지1 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지2") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지2 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지3") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지3 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지3") + .writer(메리) + .receiver(엔초) + .build(); + 엔초가_메리에게_3시에_보낸_쪽지 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("엔초가 메리에게 3시에 보낸 쪽지3") + .writer(엔초) + .receiver(메리) + .build(); 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); categoryRepository.save(전자기기_카테고리); @@ -206,7 +240,6 @@ void setUp() { List.of( 제이미가_엔초에게_1시에_보낸_쪽지, 엔초가_지토에게_2시에_보낸_쪽지, - 메리가_엔초에게_3시에_보낸_쪽지, 제이미가_엔초에게_4시에_보낸_쪽지, 엔초가_지토에게_5시에_보낸_쪽지 ) diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java new file mode 100644 index 000000000..02c48e537 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java @@ -0,0 +1,149 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.domain.repository.AuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.AuctionRepositoryImpl; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.QuerydslAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.domain.repository.BidRepository; +import com.ddang.ddang.bid.infrastructure.persistence.BidRepositoryImpl; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ChatRoomRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.MessageRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.QuerydslMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ReadMessageLogRepositoryImpl; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.domain.repository.UserRepository; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.user.infrastructure.persistence.UserRepositoryImpl; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ReadMessageLogRepositoryFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + private AuctionRepository auctionRepository; + + private UserRepository userRepository; + + private BidRepository bidRepository; + + private ChatRoomRepository chatRoomRepository; + + private MessageRepository messageRepository; + + private ReadMessageLogRepositoryImpl readMessageLogRepository; + + protected ChatRoom 메리_엔초_채팅방; + protected User 메리; + protected User 엔초; + protected ReadMessageLog 다섯_번째_메시지까지_읽은_메시지_로그; + protected Message 다섯_번째_메시지; + + protected AuctionImage 메리의_경매_대표_이미지 = new AuctionImage("메리_경매_대표_이미지.png", "메리의_경매_대표_이미지.png"); + protected AuctionImage 메리의_대표_이미지가_아닌_경매_이미지 = new AuctionImage("메리의_대표 이미지가_아닌_경매_이미지.png", "메리의_대표 이미지가_아닌_경매_이미지.png"); + protected ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + + @BeforeEach + void fixtureSetUp( + @Autowired final JPAQueryFactory jpaQueryFactory, + @Autowired final JpaAuctionRepository jpaAuctionRepository, + @Autowired final JpaUserRepository jpaUserRepository, + @Autowired final JpaBidRepository jpaBidRepository, + @Autowired final JpaChatRoomRepository jpaChatRoomRepository, + @Autowired final JpaMessageRepository jpaMessageRepository, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository + ) { + auctionRepository = new AuctionRepositoryImpl(jpaAuctionRepository, new QuerydslAuctionRepository(jpaQueryFactory)); + userRepository = new UserRepositoryImpl(jpaUserRepository); + bidRepository = new BidRepositoryImpl(jpaBidRepository); + chatRoomRepository = new ChatRoomRepositoryImpl(jpaChatRoomRepository); + messageRepository = new MessageRepositoryImpl(jpaMessageRepository, new QuerydslMessageRepository(jpaQueryFactory)); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + 메리 = User.builder() + .name("메리_판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 엔초 = User.builder() + .name("엔초_구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("14567") + .build(); + userRepository.save(메리); + userRepository.save(엔초); + + final Auction 판매자_메리_구매자_엔초_경매 = Auction.builder() + .seller(메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자_메리_구매자_엔초_경매.addAuctionImages(List.of(메리의_경매_대표_이미지, 메리의_대표_이미지가_아닌_경매_이미지)); + auctionRepository.save(판매자_메리_구매자_엔초_경매); + + + final Bid 엔초가_메리_경매에_입찰 = new Bid(판매자_메리_구매자_엔초_경매, 엔초, new BidPrice(15_000)); + bidRepository.save(엔초가_메리_경매에_입찰); + + 판매자_메리_구매자_엔초_경매.updateLastBid(엔초가_메리_경매에_입찰); + 메리_엔초_채팅방 = new ChatRoom(판매자_메리_구매자_엔초_경매, 엔초); + chatRoomRepository.save(메리_엔초_채팅방); + + List 메리_엔초_메시지들 = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + final Message 메시지 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .writer(메리) + .receiver(엔초) + .contents("안녕하세요") + .build(); + messageRepository.save(메시지); + 메리_엔초_메시지들.add(메시지); + } + + 다섯_번째_메시지 = 메리_엔초_메시지들.get(4); + 다섯_번째_메시지까지_읽은_메시지_로그 = new ReadMessageLog(메리_엔초_채팅방, 메리); + 다섯_번째_메시지까지_읽은_메시지_로그.updateLastReadMessage(다섯_번째_메시지.getId()); + + final ReadMessageLog 메리_엔초_채팅방의_메리_메시지_조회_로그 = new ReadMessageLog(메리_엔초_채팅방, 메리); + 메리_엔초_채팅방의_메리_메시지_조회_로그.updateLastReadMessage(다섯_번째_메시지.getId()); + readMessageLogRepository.saveAll(List.of(메리_엔초_채팅방의_메리_메시지_조회_로그)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java index 76b29dda0..d0f948c66 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java @@ -247,13 +247,15 @@ void setUp() { .title())), jsonPath("$.[0].lastMessage.contents", is(조회용_채팅방1.lastMessageDto() .contents())), + jsonPath("$.[0].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class), jsonPath("$.[1].id", is(조회용_채팅방2.id()), Long.class), jsonPath("$.[1].chatPartner.name", is(조회용_채팅방2.partnerDto() .name())), jsonPath("$.[1].auction.title", is(조회용_채팅방2.auctionDto() .title())), jsonPath("$.[1].lastMessage.contents", is(조회용_채팅방2.lastMessageDto() - .contents())) + .contents())), + jsonPath("$.[1].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class) ); readAllParticipatingChatRooms_문서화(resultActions); } @@ -539,6 +541,8 @@ void setUp() { .description("메시지를 보낸 시간"), fieldWithPath("[].lastMessage.contents").type(JsonFieldType.STRING) .description("메시지 내용"), + fieldWithPath("[].unreadMessageCount").type(JsonFieldType.NUMBER) + .description("안 읽은 메시지 개수"), fieldWithPath("[].isChatAvailable").type(JsonFieldType.BOOLEAN) .description("채팅 가능 여부") ) diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java index 8f7b5ddaa..6d83db0e4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java @@ -23,8 +23,8 @@ public class ChatRoomControllerFixture extends CommonControllerSliceTest { private ReadUserInChatRoomDto 구매자2 = new ReadUserInChatRoomDto(3L, "구매자2", 3L, 5.0d, false); private ReadAuctionInChatRoomDto 조회용_경매1 = new ReadAuctionInChatRoomDto(1L, "경매1", 10_000, 1L); private ReadAuctionInChatRoomDto 조회용_경매2 = new ReadAuctionInChatRoomDto(2L, "경매2", 20_000, 1L); - protected ReadChatRoomWithLastMessageDto 조회용_채팅방1 = new ReadChatRoomWithLastMessageDto(1L, 조회용_경매1, 구매자1, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자1, "메시지1"), true); - protected ReadChatRoomWithLastMessageDto 조회용_채팅방2 = new ReadChatRoomWithLastMessageDto(2L, 조회용_경매2, 구매자2, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자2, "메시지2"), true); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방1 = new ReadChatRoomWithLastMessageDto(1L, 조회용_경매1, 구매자1, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자1, "메시지1"), 1L, true); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방2 = new ReadChatRoomWithLastMessageDto(2L, 조회용_경매2, 구매자2, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자2, "메시지2"), 1L, true); protected CreateMessageRequest 메시지_생성_요청 = new CreateMessageRequest(1L, "메시지 내용"); protected CreateMessageRequest 유효하지_않은_발신자의_메시지_생성_요청 = new CreateMessageRequest(-999L, "메시지 내용"); protected CreateMessageRequest 탈퇴한_사용자와의_메시지_생성_요청 = new CreateMessageRequest(탈퇴한_사용자_아이디, "메시지 내용");
    Table 13. /auctions/{auctionId}/reviewsTable 14. /auctions/{auctionId}/reviews