Skip to content

Commit

Permalink
feat: ✏️ Append UnreadMessageCount Field in the ChatRoomResponse (#190)
Browse files Browse the repository at this point in the history
* feat: chat-message-status entity

* feat: chat_message_status_repository

* feat: impl cache_repository

* feat: impl chat message status domain business logic

* rename: add read_last_read_message_id docs about if absent data, return null

* fix: add constructor in the chat_message_status entity

* fix: add domain service validation logic

* test: chat_message_status_service unit test

* rename: add custom & impl into name because of avoidance jpa auto scan

* fix: set chat_message_status unique constraints

* test: chat_message_status_service integration test

* fix: add unread_message_count field into chat_room_res

* fix: add flow in the chat_room_search_service

* fix: join service result type tuple to triple

* fix: change chat_room_detail mapper mechanism

* fix: change flow in the use_case

* test: add test_jpa_config rabbitmq null bean

* test: all test fix

* test: chat_room_search_test

* rename: convert from chat_member_res.detail to member_detail due to swagger docs

* rename: convert from chat_res.detail to chat_detail due to swagger docs

* fix: logic modified when  get chat room detail info
  • Loading branch information
psychology50 authored Nov 4, 2024
1 parent e0f21fa commit ab6e3db
Show file tree
Hide file tree
Showing 25 changed files with 780 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ ResponseEntity<?> joinChatRoom(
@ApiResponseExplanations(errors = {
@ApiExceptionExplanation(value = ApiErrorCode.class, constant = "OVERFLOW_QUERY_PARAMETER", summary = "쿼리 파라미터 오버플로우", description = "쿼리 파라미터가 최대 개수를 초과하여 채팅방 멤버 조회에 실패했습니다.")
})
@ApiResponse(responseCode = "200", description = "채팅방 멤버 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatMembers", array = @ArraySchema(schema = @Schema(implementation = ChatMemberRes.Detail.class)))))
@ApiResponse(responseCode = "200", description = "채팅방 멤버 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatMembers", array = @ArraySchema(schema = @Schema(implementation = ChatMemberRes.MemberDetail.class)))))
ResponseEntity<?> readChatMembers(@PathVariable("chatRoomId") Long chatRoomId, @Validated @NotEmpty @RequestParam("ids") Set<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public interface ChatRoomApi {
@ApiResponse(responseCode = "200", description = "채팅방 검색 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatRooms", schema = @Schema(implementation = SliceResponseTemplate.class))))
ResponseEntity<?> searchChatRooms(@Validated ChatRoomReq.SearchQuery query, @AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "채팅방 조회", method = "GET", description = "사용자가 가입한 채팅방 중 특정 채팅방의 상세 정보를 조회한다. 채팅방의 상세 정보에는 채팅방의 참여자 목록과 최근 채팅 메시지 목록 등이 포함된다.")
@Operation(summary = "채팅방 상세 조회", method = "GET", description = "사용자가 가입한 채팅방 중 특정 채팅방의 상세 정보를 조회한다. 채팅방의 상세 정보에는 채팅방의 참여자 목록과 최근 채팅 메시지 목록 등이 포함된다.")
@Parameter(name = "chatRoomId", description = "조회할 채팅방의 식별자", example = "1", required = true)
@ApiResponse(responseCode = "200", description = "채팅방 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoom", schema = @Schema(implementation = ChatRoomRes.RoomWithParticipants.class))))
ResponseEntity<?> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @AuthenticationPrincipal SecurityUserDetails user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

public final class ChatMemberRes {
@Schema(description = "채팅방 참여자 상세 정보")
public record Detail(
public record MemberDetail(
@Schema(description = "채팅방 참여자 ID", type = "long")
Long id,
@Schema(description = "채팅방 참여자 이름")
Expand All @@ -27,8 +27,8 @@ public record Detail(
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdAt
) {
public static Detail from(ChatMember chatMember, boolean isContainNotifyEnabled) {
return new Detail(
public static MemberDetail from(ChatMember chatMember, boolean isContainNotifyEnabled) {
return new MemberDetail(
chatMember.getId(),
chatMember.getName(),
chatMember.getRole(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

public final class ChatRes {
@Schema(description = "채팅 메시지 상세 정보")
public record Detail(
public record ChatDetail(
@Schema(description = "채팅방 ID", type = "long")
Long chatRoomId,
@Schema(description = "채팅 ID", type = "long")
Expand All @@ -30,8 +30,8 @@ public record Detail(
@Schema(description = "채팅 보낸 사람 ID", type = "long")
Long senderId
) {
public static Detail from(ChatMessage message) {
return new Detail(
public static ChatDetail from(ChatMessage message) {
return new ChatDetail(
message.getChatRoomId(),
message.getChatId(),
message.getContent(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ public record Detail(
@Schema(description = "채팅방 개설일")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdAt
LocalDateTime createdAt,
@Schema(description = "읽지 않은 메시지 수. 100 이상의 값을 가지면, 100으로 표시된다.")
long unreadMessageCount
) {
public Detail(Long id, String title, String description, String backgroundImageUrl, boolean isPrivate, boolean isAdmin, int participantCount, LocalDateTime createdAt) {
public Detail(Long id, String title, String description, String backgroundImageUrl, boolean isPrivate, boolean isAdmin, int participantCount, LocalDateTime createdAt, long unreadMessageCount) {
this.id = id;
this.title = title;
this.description = Objects.toString(description, "");
Expand All @@ -43,9 +45,10 @@ public Detail(Long id, String title, String description, String backgroundImageU
this.isAdmin = isAdmin;
this.participantCount = participantCount;
this.createdAt = createdAt;
this.unreadMessageCount = (unreadMessageCount > 100) ? 100 : unreadMessageCount;
}

public static Detail from(ChatRoom chatRoom, boolean isAdmin, int participantCount) {
public static Detail of(ChatRoom chatRoom, boolean isAdmin, int participantCount, long unreadMessageCount) {
return new Detail(
chatRoom.getId(),
chatRoom.getTitle(),
Expand All @@ -54,7 +57,8 @@ public static Detail from(ChatRoom chatRoom, boolean isAdmin, int participantCou
chatRoom.getPassword() != null,
isAdmin,
participantCount,
chatRoom.getCreatedAt()
chatRoom.getCreatedAt(),
unreadMessageCount
);
}
}
Expand All @@ -70,13 +74,13 @@ public record Summary(
@Builder
public record RoomWithParticipants(
@Schema(description = "채팅방에서 내 정보")
ChatMemberRes.Detail myInfo,
ChatMemberRes.MemberDetail myInfo,
@Schema(description = "최근에 채팅 메시지를 보낸 참여자의 상세 정보 목록")
List<ChatMemberRes.Detail> recentParticipants,
List<ChatMemberRes.MemberDetail> recentParticipants,
@Schema(description = "채팅방에서 내 정보와 최근 활동자를 제외한 참여자 ID 목록")
List<Long> otherParticipantIds,
@Schema(description = "최근 채팅 이력. 메시지는 최신순으로 정렬되어 반환.")
List<ChatRes.Detail> recentMessages
List<ChatRes.ChatDetail> recentMessages
) {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

@Mapper
public final class ChatMemberMapper {
public static List<ChatMemberRes.Detail> toChatMemberResDetail(List<ChatMember> chatMembers) {
public static List<ChatMemberRes.MemberDetail> toChatMemberResDetail(List<ChatMember> chatMembers) {
return chatMembers.stream()
.map(chatMember -> ChatMemberRes.Detail.from(chatMember, false))
.map(chatMember -> ChatMemberRes.MemberDetail.from(chatMember, false))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,36 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Mapper
public final class ChatRoomMapper {


public static SliceResponseTemplate<ChatRoomRes.Detail> toChatRoomResDetails(Slice<ChatRoomDetail> details, Pageable pageable) {
List<ChatRoomRes.Detail> contents = toChatRoomResDetails(details.getContent());
List<ChatRoomRes.Detail> contents = new ArrayList<>();
for (ChatRoomDetail detail : details.getContent()) {
contents.add(
new ChatRoomRes.Detail(
detail.id(),
detail.title(),
detail.description(),
detail.backgroundImageUrl(),
detail.password() != null,
detail.isAdmin(),
detail.participantCount(),
detail.createdAt(),
0
)
);
}

return SliceResponseTemplate.of(contents, pageable, contents.size(), details.hasNext());
}

public static List<ChatRoomRes.Detail> toChatRoomResDetails(List<ChatRoomDetail> details) {
public static List<ChatRoomRes.Detail> toChatRoomResDetails(Map<ChatRoomDetail, Long> details) {
List<ChatRoomRes.Detail> responses = new ArrayList<>();

for (ChatRoomDetail detail : details) {
for (Map.Entry<ChatRoomDetail, Long> entry : details.entrySet()) {
ChatRoomDetail detail = entry.getKey();
responses.add(
new ChatRoomRes.Detail(
detail.id(),
Expand All @@ -38,29 +53,30 @@ public static List<ChatRoomRes.Detail> toChatRoomResDetails(List<ChatRoomDetail>
detail.password() != null,
detail.isAdmin(),
detail.participantCount(),
detail.createdAt()
detail.createdAt(),
entry.getValue()
)
);
}

return responses;
}

public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, boolean isAdmin, int participantCount) {
return ChatRoomRes.Detail.from(chatRoom, isAdmin, participantCount);
public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, boolean isAdmin, int participantCount, long unreadMessageCount) {
return ChatRoomRes.Detail.of(chatRoom, isAdmin, participantCount, unreadMessageCount);
}

public static ChatRoomRes.RoomWithParticipants toChatRoomResRoomWithParticipants(ChatMember myInfo, List<ChatMember> recentParticipants, List<Long> otherMemberIds, List<ChatMessage> chatMessages) {
List<ChatMemberRes.Detail> recentParticipantsRes = recentParticipants.stream()
.map(participant -> ChatMemberRes.Detail.from(participant, false))
List<ChatMemberRes.MemberDetail> recentParticipantsRes = recentParticipants.stream()
.map(participant -> ChatMemberRes.MemberDetail.from(participant, false))
.toList();

List<ChatRes.Detail> chatMessagesRes = chatMessages.stream()
.map(ChatRes.Detail::from)
List<ChatRes.ChatDetail> chatMessagesRes = chatMessages.stream()
.map(ChatRes.ChatDetail::from)
.toList();

return ChatRoomRes.RoomWithParticipants.builder()
.myInfo(ChatMemberRes.Detail.from(myInfo, true))
.myInfo(ChatMemberRes.MemberDetail.from(myInfo, true))
.recentParticipants(recentParticipantsRes)
.otherParticipantIds(otherMemberIds)
.recentMessages(chatMessagesRes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.co.pennyway.api.apis.chat.service;

import kr.co.pennyway.domain.common.redis.message.service.ChatMessageService;
import kr.co.pennyway.domain.common.redisson.DistributedLock;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import kr.co.pennyway.domain.domains.chatroom.exception.ChatRoomErrorCode;
Expand All @@ -14,7 +15,8 @@
import kr.co.pennyway.infra.common.event.ChatRoomJoinEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

Expand All @@ -28,6 +30,8 @@ public class ChatMemberJoinService {
private final ChatRoomService chatRoomService;
private final ChatMemberService chatMemberService;

private final ChatMessageService chatMessageService;

private final ApplicationEventPublisher eventPublisher;

/**
Expand All @@ -37,10 +41,10 @@ public class ChatMemberJoinService {
* @param userId Long : 가입하려는 사용자의 ID
* @param chatRoomId Long : 가입하려는 채팅방의 ID
* @param password Integer : 비공개 채팅방의 경우 비밀번호 정보를 입력받으며, 채팅방에 비밀번호가 없을 경우 null
* @return Pair<ChatRoom, Integer> - 채팅방 정보와 현재 가입한 회원 수
* @return Triple<ChatRoom, Integer, Long> : 가입한 채팅방 정보, 현재 채팅방의 회원 수, 읽지 않은 메시지
*/
@DistributedLock(key = "'chat-room-join-' + #chatRoomId")
public Pair<ChatRoom, Integer> execute(Long userId, Long chatRoomId, Integer password) {
public Triple<ChatRoom, Integer, Long> execute(Long userId, Long chatRoomId, Integer password) {
ChatRoom chatRoom = chatRoomService.readChatRoom(chatRoomId).orElseThrow(() -> new ChatRoomErrorException(ChatRoomErrorCode.NOT_FOUND_CHAT_ROOM));

Long currentMemberCount = chatMemberService.countActiveMembers(chatRoomId);
Expand All @@ -56,10 +60,11 @@ public Pair<ChatRoom, Integer> execute(Long userId, Long chatRoomId, Integer pas

User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));
ChatMember member = chatMemberService.createMember(user, chatRoom);
Long unreadMessageCount = chatMessageService.countUnreadMessages(chatRoomId, 0L);

eventPublisher.publishEvent(ChatRoomJoinEvent.of(chatRoomId, member.getName()));

return Pair.of(chatRoom, currentMemberCount.intValue() + 1);
return ImmutableTriple.of(chatRoom, currentMemberCount.intValue() + 1, unreadMessageCount);
}

private boolean isFullRoom(Long currentMemberCount) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
package kr.co.pennyway.api.apis.chat.service;

import kr.co.pennyway.domain.common.redis.message.service.ChatMessageService;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomService;
import kr.co.pennyway.domain.domains.chatstatus.service.ChatMessageStatusService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatRoomSearchService {
private final ChatRoomService chatRoomService;
private final ChatMessageStatusService chatMessageStatusService;
private final ChatMessageService chatMessageService;

/**
* 사용자 ID가 속한 채팅방 목록을 조회한다.
*
* @return 채팅방 목록 (채팅방 정보, 읽지 않은 메시지 수)
*/
@Transactional(readOnly = true)
public List<ChatRoomDetail> readChatRooms(Long userId) {
return chatRoomService.readChatRoomsByUserId(userId);
public Map<ChatRoomDetail, Long> readChatRooms(Long userId) {
List<ChatRoomDetail> chatRooms = chatRoomService.readChatRoomsByUserId(userId);
Map<ChatRoomDetail, Long> result = new HashMap<>();

for (ChatRoomDetail chatRoom : chatRooms) {
Long lastReadMessageId = chatMessageStatusService.readLastReadMessageId(userId, chatRoom.id());
Long unreadCount = chatMessageService.countUnreadMessages(chatRoom.id(), lastReadMessageId);
result.put(chatRoom, unreadCount);
}

return result;
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import java.util.List;
import java.util.Set;
Expand All @@ -24,12 +24,12 @@ public class ChatMemberUseCase {
private final ChatMemberSearchService chatMemberSearchService;

public ChatRoomRes.Detail joinChatRoom(Long userId, Long chatRoomId, Integer password) {
Pair<ChatRoom, Integer> chatRoom = chatMemberJoinService.execute(userId, chatRoomId, password);
Triple<ChatRoom, Integer, Long> chatRoom = chatMemberJoinService.execute(userId, chatRoomId, password);

return ChatRoomMapper.toChatRoomResDetail(chatRoom.getLeft(), false, chatRoom.getRight());
return ChatRoomMapper.toChatRoomResDetail(chatRoom.getLeft(), false, chatRoom.getMiddle(), chatRoom.getRight());
}

public List<ChatMemberRes.Detail> readChatMembers(Long chatRoomId, Set<Long> chatMemberIds) {
public List<ChatMemberRes.MemberDetail> readChatMembers(Long chatRoomId, Set<Long> chatMemberIds) {
List<ChatMember> chatMembers = chatMemberSearchService.readChatMembers(chatRoomId, chatMemberIds);

return ChatMemberMapper.toChatMemberResDetail(chatMembers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.data.domain.Slice;

import java.util.List;
import java.util.Map;
import java.util.Set;

@UseCase
Expand All @@ -30,11 +31,11 @@ public class ChatRoomUseCase {
public ChatRoomRes.Detail createChatRoom(ChatRoomReq.Create request, Long userId) {
ChatRoom chatRoom = chatRoomSaveService.createChatRoom(request, userId);

return ChatRoomMapper.toChatRoomResDetail(chatRoom, true, 1);
return ChatRoomMapper.toChatRoomResDetail(chatRoom, true, 1, 0);
}

public List<ChatRoomRes.Detail> getChatRooms(Long userId) {
List<ChatRoomDetail> chatRooms = chatRoomSearchService.readChatRooms(userId);
Map<ChatRoomDetail, Long> chatRooms = chatRoomSearchService.readChatRooms(userId);

return ChatRoomMapper.toChatRoomResDetails(chatRooms);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void successReadChatMembers() throws Exception {
// given
Long chatRoomId = 1L;
Set<Long> memberIds = Set.of(1L, 2L, 3L);
List<ChatMemberRes.Detail> expectedResponse = createMockMemberDetails();
List<ChatMemberRes.MemberDetail> expectedResponse = createMockMemberDetails();

given(chatMemberUseCase.readChatMembers(chatRoomId, memberIds)).willReturn(expectedResponse);

Expand Down Expand Up @@ -116,11 +116,11 @@ void failReadChatMembersWhenIdsIsEmpty() throws Exception {
.andDo(print());
}

private List<ChatMemberRes.Detail> createMockMemberDetails() {
private List<ChatMemberRes.MemberDetail> createMockMemberDetails() {
return List.of(
new ChatMemberRes.Detail(1L, "User1", ChatMemberRole.MEMBER, null, LocalDateTime.now()),
new ChatMemberRes.Detail(2L, "User2", ChatMemberRole.MEMBER, null, LocalDateTime.now()),
new ChatMemberRes.Detail(3L, "User3", ChatMemberRole.MEMBER, null, LocalDateTime.now())
new ChatMemberRes.MemberDetail(1L, "User1", ChatMemberRole.MEMBER, null, LocalDateTime.now()),
new ChatMemberRes.MemberDetail(2L, "User2", ChatMemberRole.MEMBER, null, LocalDateTime.now()),
new ChatMemberRes.MemberDetail(3L, "User3", ChatMemberRole.MEMBER, null, LocalDateTime.now())
);
}
}
Loading

0 comments on commit ab6e3db

Please sign in to comment.