diff --git a/be/src/main/java/kr/codesquad/chat/controller/ChatController.java b/be/src/main/java/kr/codesquad/chat/controller/ChatController.java index c803b9183..1f490ce5e 100644 --- a/be/src/main/java/kr/codesquad/chat/controller/ChatController.java +++ b/be/src/main/java/kr/codesquad/chat/controller/ChatController.java @@ -9,15 +9,15 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import kr.codesquad.chat.dto.request.ChatMessageRequest; import kr.codesquad.chat.dto.request.ChatRoomCreateRequest; -import kr.codesquad.chat.dto.request.SendMessageRequest; import kr.codesquad.chat.dto.response.ChatRoomCreateResponse; +import kr.codesquad.chat.dto.response.ChatRoomListResponse; import kr.codesquad.chat.service.ChatService; import kr.codesquad.util.Constants; -import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -26,6 +26,13 @@ public class ChatController { private final ChatService chatService; + @GetMapping("/api/chatrooms") + public ResponseEntity showAllUsersChatRooms(@RequestParam(required = false) Long itemId, + HttpServletRequest request) { + String loginId = (String)request.getAttribute(Constants.LOGIN_ID); + return ResponseEntity.ok().body(chatService.findChatRoomsBy(itemId, loginId)); + } + @PostMapping("/api/chatrooms") public ResponseEntity createChatRoom( @RequestBody ChatRoomCreateRequest chatRoomCreateRequest, diff --git a/be/src/main/java/kr/codesquad/chat/dto/response/ChatMemberDataResponse.java b/be/src/main/java/kr/codesquad/chat/dto/response/ChatMemberDataResponse.java new file mode 100644 index 000000000..cbaf46d85 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/response/ChatMemberDataResponse.java @@ -0,0 +1,19 @@ +package kr.codesquad.chat.dto.response; + +import kr.codesquad.chat.dto.vo.ChatRoomListVo; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ChatMemberDataResponse { + private Long id; + private String profileImageUrl; + private String nickname; + + public static ChatMemberDataResponse from(ChatRoomListVo chatRoomListVo) { + return ChatMemberDataResponse.builder().id(chatRoomListVo.getSenderId()).profileImageUrl( + chatRoomListVo.getSenderProfileImageUrl()).nickname(chatRoomListVo.getSenderNickname()).build(); + } + +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/response/ChatRoomListResponse.java b/be/src/main/java/kr/codesquad/chat/dto/response/ChatRoomListResponse.java new file mode 100644 index 000000000..c119354f2 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/response/ChatRoomListResponse.java @@ -0,0 +1,12 @@ +package kr.codesquad.chat.dto.response; + +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ChatRoomListResponse { + private final List chatRooms; +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/response/ItemDataResponse.java b/be/src/main/java/kr/codesquad/chat/dto/response/ItemDataResponse.java new file mode 100644 index 000000000..c5267c47e --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/response/ItemDataResponse.java @@ -0,0 +1,17 @@ +package kr.codesquad.chat.dto.response; + +import kr.codesquad.chat.dto.vo.ChatRoomListVo; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ItemDataResponse { + private Long id; + private String thumbnailUrl; + + public static ItemDataResponse from(ChatRoomListVo chatRoomListVo) { + return ItemDataResponse.builder().id(chatRoomListVo.getItemId()).thumbnailUrl( + chatRoomListVo.getItemThumbnailUrl()).build(); + } +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/response/UserChatRoomListResponse.java b/be/src/main/java/kr/codesquad/chat/dto/response/UserChatRoomListResponse.java new file mode 100644 index 000000000..6c9ffe5c7 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/response/UserChatRoomListResponse.java @@ -0,0 +1,30 @@ +package kr.codesquad.chat.dto.response; + +import java.time.ZonedDateTime; +import java.util.Map; + +import kr.codesquad.chat.dto.vo.ChatRoomListVo; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserChatRoomListResponse { + private Long id; + private ChatMemberDataResponse chatMember; + private String recentMessage; + private int unreadCount; + private ZonedDateTime updatedAt; + private ItemDataResponse item; + + public static UserChatRoomListResponse of(ChatRoomListVo chatRoom, Map unreadCounts) { + return UserChatRoomListResponse.builder() + .id(chatRoom.getId()) + .chatMember(ChatMemberDataResponse.from(chatRoom)) + .recentMessage(chatRoom.getRecentMessage()) + .unreadCount(unreadCounts.getOrDefault(chatRoom.getId(), 0)) + .updatedAt(chatRoom.getUpdatedAt()) + .item(ItemDataResponse.from(chatRoom)) + .build(); + } +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/vo/ChatMessageCountDataVo.java b/be/src/main/java/kr/codesquad/chat/dto/vo/ChatMessageCountDataVo.java new file mode 100644 index 000000000..263cf5476 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/vo/ChatMessageCountDataVo.java @@ -0,0 +1,20 @@ +package kr.codesquad.chat.dto.vo; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ChatMessageCountDataVo { + private Long chatRoomId; + private boolean isRead; + private Long senderId; + + @Builder + public ChatMessageCountDataVo(Long chatRoomId, boolean isRead, Long senderId) { + this.chatRoomId = chatRoomId; + this.isRead = isRead; + this.senderId = senderId; + } +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/vo/ChatRoomListVo.java b/be/src/main/java/kr/codesquad/chat/dto/vo/ChatRoomListVo.java new file mode 100644 index 000000000..56ea61106 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/vo/ChatRoomListVo.java @@ -0,0 +1,36 @@ +package kr.codesquad.chat.dto.vo; + +import java.time.ZonedDateTime; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ChatRoomListVo { + private Long id; + private Long senderId; + private String senderProfileImageUrl; + private String senderNickname; + private String recentMessage; + private int unreadCount; + private ZonedDateTime updatedAt; + private Long itemId; + private String itemThumbnailUrl; + + @Builder + public ChatRoomListVo(Long id, Long senderId, String senderProfileImageUrl, String senderNickname, + String recentMessage, + int unreadCount, ZonedDateTime updatedAt, Long itemId, String itemThumbnailUrl) { + this.id = id; + this.senderId = senderId; + this.senderProfileImageUrl = senderProfileImageUrl; + this.senderNickname = senderNickname; + this.recentMessage = recentMessage; + this.unreadCount = unreadCount; + this.updatedAt = updatedAt; + this.itemId = itemId; + this.itemThumbnailUrl = itemThumbnailUrl; + } +} diff --git a/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java b/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java index 28b6066ca..ce621aa9c 100644 --- a/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java +++ b/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java @@ -22,11 +22,21 @@ public class ChatMessage extends TimeStamped { private Long chatRoomId; @Column(nullable = false, length = 200) private String content; + @Column(columnDefinition = "BIT DEFAULT 0") + private boolean isRead; + @Column(nullable = false) + private Long senderId; @Builder - public ChatMessage(Long id, Long chatRoomId, String content) { + public ChatMessage(Long id, Long chatRoomId, String content, boolean isRead, Long senderId) { this.id = id; this.chatRoomId = chatRoomId; this.content = content; + this.isRead = isRead; + this.senderId = senderId; + } + + public void readMessage() { + this.isRead = true; } } diff --git a/be/src/main/java/kr/codesquad/chat/repository/ChatRoomListRepository.java b/be/src/main/java/kr/codesquad/chat/repository/ChatRoomListRepository.java new file mode 100644 index 000000000..5e9355a6e --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/repository/ChatRoomListRepository.java @@ -0,0 +1,79 @@ +package kr.codesquad.chat.repository; + +import static kr.codesquad.chat.entity.QChatMessage.*; +import static kr.codesquad.chat.entity.QChatRoom.*; +import static kr.codesquad.item.entity.QItem.*; +import static kr.codesquad.user.entity.QUser.*; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import kr.codesquad.chat.dto.vo.ChatMessageCountDataVo; +import kr.codesquad.chat.dto.vo.ChatRoomListVo; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Repository +public class ChatRoomListRepository { + private final JPAQueryFactory queryFactory; + + public List findAllBy(Long itemId, Long userId) { + return queryFactory + .select(Projections.fields(ChatRoomListVo.class, + chatRoom.id, + user.id.as("senderId"), + user.profileImageUrl.as("senderProfileImageUrl"), + user.nickname.as("senderNickname"), + item.id.as("itemId"), + item.thumbnailUrl.as("itemThumbnailUrl"), + chatMessage.content.as("recentMessage"), + chatMessage.createdAt.as("updatedAt") + )) + .from(chatRoom) + .leftJoin(user).on(chatRoom.senderId.eq(user.id)) + .leftJoin(item).on(chatRoom.itemId.eq(item.id)) + .leftJoin(chatMessage).on( + chatMessage.chatRoomId.eq(chatRoom.id) + .and(chatMessage.createdAt.eq( + JPAExpressions + .select(chatMessage.createdAt.max()) + .from(chatMessage) + .where(chatMessage.chatRoomId.eq(chatRoom.id)) + )) + ) + .where(item.userId.eq(userId).or(chatRoom.senderId.eq(userId)), equalItemId(itemId)).fetch(); + } + + public List countUnreadChatMessage(Long userId) { + return queryFactory + .select(Projections.fields(ChatMessageCountDataVo.class, + chatRoom.id.as("chatRoomId"), + chatMessage.isRead, + chatMessage.senderId + )) + .from(chatRoom) + .leftJoin(item).on(chatRoom.itemId.eq(item.id)) + .leftJoin(chatMessage).on(chatMessage.chatRoomId.eq(chatRoom.id)) + .where(chatRoom.senderId.eq(userId).or(item.userId.eq(userId)), chatMessage.isRead.eq(false)) + .fetch(); + } + + private BooleanExpression equalItemId(Long itemId) { + if (itemId == null) { + return null; + } + + return item.id.eq(itemId); + } + + private BooleanExpression equalSenderId(Long userId) { + return chatRoom.senderId.eq(userId); + } + +} diff --git a/be/src/main/java/kr/codesquad/chat/service/ChatService.java b/be/src/main/java/kr/codesquad/chat/service/ChatService.java index ab26fa394..023a71db2 100644 --- a/be/src/main/java/kr/codesquad/chat/service/ChatService.java +++ b/be/src/main/java/kr/codesquad/chat/service/ChatService.java @@ -1,5 +1,10 @@ package kr.codesquad.chat.service; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.stereotype.Service; @@ -10,10 +15,14 @@ import kr.codesquad.chat.dto.ChatMapper; import kr.codesquad.chat.dto.request.ChatMessageRequest; import kr.codesquad.chat.dto.request.ChatRoomCreateRequest; -import kr.codesquad.chat.dto.request.SendMessageRequest; import kr.codesquad.chat.dto.response.ChatRoomCreateResponse; +import kr.codesquad.chat.dto.response.ChatRoomListResponse; +import kr.codesquad.chat.dto.response.UserChatRoomListResponse; +import kr.codesquad.chat.dto.vo.ChatMessageCountDataVo; +import kr.codesquad.chat.dto.vo.ChatRoomListVo; import kr.codesquad.chat.entity.ChatMessage; import kr.codesquad.chat.repository.ChatMessageRepository; +import kr.codesquad.chat.repository.ChatRoomListRepository; import kr.codesquad.chat.repository.ChatRoomRepository; import kr.codesquad.user.entity.User; import kr.codesquad.user.repository.UserRepository; @@ -31,6 +40,7 @@ public class ChatService { private final ObjectMapper objectMapper; private final ChannelTopic channelTopic; private final RedisTemplate redisTemplate; + private final ChatRoomListRepository chatRoomListRepository; public ChatRoomCreateResponse createRoom(ChatRoomCreateRequest chatRoomCreateRequest, String loginId) { User user = userRepository.findByLoginId(loginId); @@ -42,9 +52,41 @@ public ChatRoomCreateResponse createRoom(ChatRoomCreateRequest chatRoomCreateReq public void sendMessage(ChatMessageRequest chatMessageRequest, Long chatRoomId) { //채팅 생성 및 저장 - ChatMessage chatMessage = chatMessageRepository.save(ChatMapper.INSTANCE.toChatMessage(chatMessageRequest, chatRoomId)); + // TODO: 채팅방에 사람이 있다면 ChatMessage 읽음 처리 로직 추가 + ChatMessage chatMessage = chatMessageRepository.save( + ChatMapper.INSTANCE.toChatMessage(chatMessageRequest, chatRoomId)); String topic = channelTopic.getTopic(); - redisTemplate.convertAndSend(topic, ChatMapper.INSTANCE.toSendMessageRequest(chatMessage, chatMessageRequest.getSenderId())); + redisTemplate.convertAndSend(topic, + ChatMapper.INSTANCE.toSendMessageRequest(chatMessage, chatMessageRequest.getSenderId())); + } + + public ChatRoomListResponse findChatRoomsBy(Long itemId, String loginId) { + Long userId = userRepository.findByLoginId(loginId).getId(); + List chatRoomListVos = chatRoomListRepository.findAllBy(itemId, userId); + List chatMessageCountDataVos = chatRoomListRepository.countUnreadChatMessage( + userId); // unreadCount만 따로 조회 + + Map unreadChatMessageCountMap = countUnreadChatMessage(chatMessageCountDataVos, userId); + + // chatRoomListVos에 맞는 unreadCount를 넣어줌. + List userChatRoomListResponses = chatRoomListVos.stream() + .map(chatRoom -> UserChatRoomListResponse.of(chatRoom, unreadChatMessageCountMap)) + .collect( + Collectors.toUnmodifiableList()); + + return ChatRoomListResponse.builder().chatRooms(userChatRoomListResponses).build(); + } + + private Map countUnreadChatMessage(List chatMessageCountDataVos, + Long userId) { + Map map = new HashMap<>(); + for (ChatMessageCountDataVo chatMessageCountDataVo : chatMessageCountDataVos) { + if (chatMessageCountDataVo.getSenderId() != userId && !chatMessageCountDataVo.isRead()) { + map.put(chatMessageCountDataVo.getChatRoomId(), + map.getOrDefault(chatMessageCountDataVo.getChatRoomId(), 0) + 1); + } + } + return map; } } diff --git a/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java b/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java index f60a4a389..437c5f666 100644 --- a/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java +++ b/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java @@ -25,7 +25,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/api/ws") - .setAllowedOriginPatterns("*"); + .setAllowedOriginPatterns("*").withSockJS(); } @Override