Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

안 읽은 메시지 카운팅 기능 구현 #709

Merged
merged 32 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
96b54d0
feat: 읽은 메시지 로그 도메인 생성
swonny Oct 16, 2023
aabc0ef
feat: 읽은 메시지 로그 저장 레포지토리 생성
swonny Oct 16, 2023
de9e5e9
refactor: 컬럼명 변경
swonny Oct 16, 2023
c773023
feat: 메시지 조회 시 마지막으로 읽은 메시지 로그 저장
swonny Oct 16, 2023
ce34598
feat: 채팅방 목록 조회 시 읽지 않은 메시지 개수를 포함하는 기능 추가
swonny Oct 17, 2023
d90c03c
feat: 채팅방 생성 시 참여자에 대한 메시지 로그 생성하는 기능 추가
swonny Oct 17, 2023
3f8d431
feat: 경매 아이디로 채팅방 조회하는 기능 추가
swonny Oct 17, 2023
e0ef4e8
feat: 채팅방 목록 조회 시 반환값에 안 읽은 메시지 개수 추가
swonny Oct 17, 2023
5e22986
refactor: 메시지 로그 조회 네이밍 변경
swonny Oct 17, 2023
b14e7ab
feat: 어노테이션 추가
swonny Oct 17, 2023
049fa04
feat: 로그 찾지 못한 경우에 대한 커스텀 예외 추가
swonny Oct 17, 2023
0e72381
refactor: 로그 생성 로직 이벤트로 분리
swonny Oct 17, 2023
0b1e25c
refactor: 불필요한 메서드 삭제
swonny Oct 17, 2023
066570b
test: 생략한 테스트 추가
swonny Oct 17, 2023
b4d03e3
refactor: 채팅방과 메시지 조회 로그cascade type 지정
swonny Oct 17, 2023
e7993c6
refactor: 불필요한 파라미터 삭제
swonny Oct 17, 2023
6710648
Merge remote-tracking branch 'origin/develop-be' into feature/677
swonny Oct 17, 2023
824d5c2
feat: flyway 스크립트 작성
swonny Oct 17, 2023
a1e3309
refactor: 불필요한 어노테이션 삭제
swonny Oct 31, 2023
fbaf37d
refactor: 개행 추가 및 분기문 스트림으로 대체
swonny Oct 31, 2023
35e3b8a
feat: 안 읽은 메시지 개수 컨트롤러 업데이트
swonny Oct 31, 2023
8cc690a
Merge remote-tracking branch 'origin/develop-be' into feature/677
swonny Oct 31, 2023
83aa31c
refactor: 불필요한 필드 삭제
swonny Nov 1, 2023
688d8e2
refactor: 불필요한 join이 발생하는 쿼리 삭제
swonny Nov 1, 2023
591ccd5
refactor: 불필요한 import문 삭제
swonny Nov 1, 2023
3019893
refactor: 메시지 전송과 읽음 저장 트랜잭션 분리
swonny Nov 3, 2023
3bec36d
refactor: 불필요한 어노테이션 삭제, 자동 정렬
swonny Nov 3, 2023
7102287
test: 예외 케이스 테스트 추가
swonny Nov 3, 2023
fb80954
Merge remote-tracking branch 'origin/develop-be' into feature/677
swonny Nov 3, 2023
dbad4b3
refactor: 업데이트에 적절한 변수명으로 변경
swonny Nov 4, 2023
461d34d
refactor: 레포지토리 저장 시 여러 개 저장하는 메서드 생성
swonny Nov 4, 2023
c299d27
chore: 잘못된 flyway 스크림트 수정
swonny Nov 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택

개행..?

}

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)
Comment on lines +31 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

칭찬

믿겠습니다....메리...

)).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
Loading