Skip to content

Commit

Permalink
feat: #677 안 읽은 메시지 카운팅 기능 구현 (#709)
Browse files Browse the repository at this point in the history
* feat: 읽은 메시지 로그 도메인 생성

* feat: 읽은 메시지 로그 저장 레포지토리 생성

* refactor: 컬럼명 변경

* feat: 메시지 조회 시 마지막으로 읽은 메시지 로그 저장

* feat: 채팅방 목록 조회 시 읽지 않은 메시지 개수를 포함하는 기능 추가

* feat: 채팅방 생성 시 참여자에 대한 메시지 로그 생성하는 기능 추가

* feat: 경매 아이디로 채팅방 조회하는 기능 추가

* feat: 채팅방 목록 조회 시 반환값에 안 읽은 메시지 개수 추가

* refactor: 메시지 로그 조회 네이밍 변경

* feat: 어노테이션 추가

* feat: 로그 찾지 못한 경우에 대한 커스텀 예외 추가

* refactor: 로그 생성 로직 이벤트로 분리

* refactor: 불필요한 메서드 삭제

* test: 생략한 테스트 추가

* refactor: 채팅방과 메시지 조회 로그cascade type 지정

* refactor: 불필요한 파라미터 삭제

* feat: flyway 스크립트 작성

* refactor: 불필요한 어노테이션 삭제

* refactor: 개행 추가 및 분기문 스트림으로 대체

* feat: 안 읽은 메시지 개수 컨트롤러 업데이트

* refactor: 불필요한 필드 삭제

* refactor: 불필요한 join이 발생하는 쿼리 삭제

* refactor: 불필요한 import문 삭제

* refactor: 메시지 전송과 읽음 저장 트랜잭션 분리

* refactor: 불필요한 어노테이션 삭제, 자동 정렬

* test: 예외 케이스 테스트 추가

* refactor: 업데이트에 적절한 변수명으로 변경

* refactor: 레포지토리 저장 시 여러 개 저장하는 메서드 생성

* chore: 잘못된 flyway 스크림트 수정
  • Loading branch information
swonny authored Nov 6, 2023
1 parent f868d3a commit 4d4c3f7
Show file tree
Hide file tree
Showing 36 changed files with 1,238 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<ReadMessageDto> 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(
"지정한 아이디에 대한 채팅방을 찾을 수 없습니다."));
Expand All @@ -77,6 +75,12 @@ public List<ReadMessageDto> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record ReadChatRoomWithLastMessageDto(
ReadAuctionInChatRoomDto auctionDto,
ReadUserInChatRoomDto partnerDto,
ReadLastMessageDto lastMessageDto,
Long unreadMessageCount,
boolean isChatAvailable
) {

Expand All @@ -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)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ddang.ddang.chat.application.event;

import com.ddang.ddang.chat.domain.ChatRoom;

public record CreateReadMessageLogEvent(ChatRoom chatRoom) {
}
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ddang.ddang.chat.application.exception;

public class ReadMessageLogNotFoundException extends IllegalArgumentException {

public ReadMessageLogNotFoundException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<ReadMessageLog> findBy(final Long readerId, final Long chatRoomId);

List<ReadMessageLog> saveAll(List<ReadMessageLog> readMessageLogs);
}
Original file line number Diff line number Diff line change
@@ -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<ReadMessageLog, Long> {

@Query("""
SELECT rml
FROM ReadMessageLog rml
WHERE rml.chatRoom.id = :chatRoomId AND rml.reader.id = :readerId
""")
Optional<ReadMessageLog> findLastReadMessageByUserIdAndChatRoomId(final Long readerId, final Long chatRoomId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -26,8 +28,12 @@ public class QuerydslChatRoomAndMessageAndImageRepository {

public List<ChatRoomAndMessageAndImageDto> findAllChatRoomInfoByUserIdOrderByLastMessage(final Long userId) {
final List<ChatRoomAndMessageAndImageQueryProjectionDto> 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()
Expand All @@ -52,6 +58,21 @@ public List<ChatRoomAndMessageAndImageDto> findAllChatRoomInfoByUserIdOrderByLas
return sortByLastMessageIdDesc(unsortedDtos);
}

private static JPQLQuery<Long> 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<ChatRoomAndMessageAndImageDto> sortByLastMessageIdDesc(
final List<ChatRoomAndMessageAndImageQueryProjectionDto> unsortedDtos
) {
Expand Down
Loading

0 comments on commit 4d4c3f7

Please sign in to comment.