diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java index dd53ac2c..d30e0be2 100644 --- a/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java @@ -13,7 +13,7 @@ import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.dto.CreateChatRoomRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -38,7 +38,7 @@ public interface ChatApi { """) @PostMapping("/rooms") ResponseEntity createOrGetChatRoom( - @Valid @RequestBody CreateChatRoomRequest request, + @Valid @RequestBody ChatRoomCreateRequest request, @UserId Integer userId ); @@ -52,7 +52,9 @@ ResponseEntity createOrGetChatRoom( - 최근 메시지가 있는 순서대로 정렬됩니다. """) @GetMapping("/rooms") - ResponseEntity getChatRooms(@UserId Integer userId); + ResponseEntity getChatRooms( + @UserId Integer userId + ); @Operation(summary = "문의하기 메시지 리스트를 조회한다.", description = """ ## 설명 diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java index 45c7cf30..376c6ded 100644 --- a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java @@ -14,8 +14,8 @@ import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.dto.CreateChatRoomRequest; -import gg.agit.konect.domain.chat.service.ChatRoomService; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -25,20 +25,22 @@ @RequestMapping("/chats") public class ChatController implements ChatApi { - private final ChatRoomService chatRoomService; + private final ChatService chatService; @PostMapping("/rooms") public ResponseEntity createOrGetChatRoom( - @Valid @RequestBody CreateChatRoomRequest request, + @Valid @RequestBody ChatRoomCreateRequest request, @UserId Integer userId ) { - ChatRoomResponse response = chatRoomService.createOrGetChatRoom(userId, request); + ChatRoomResponse response = chatService.createOrGetChatRoom(userId, request); return ResponseEntity.ok(response); } @GetMapping("/rooms") - public ResponseEntity getChatRooms(@UserId Integer userId) { - ChatRoomsResponse response = chatRoomService.getChatRooms(userId); + public ResponseEntity getChatRooms( + @UserId Integer userId + ) { + ChatRoomsResponse response = chatService.getChatRooms(userId); return ResponseEntity.ok(response); } @@ -49,7 +51,7 @@ public ResponseEntity getChatRoomMessages( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - ChatMessagesResponse response = chatRoomService.getChatRoomMessages(userId, chatRoomId, page, limit); + ChatMessagesResponse response = chatService.getChatRoomMessages(userId, chatRoomId, page, limit); return ResponseEntity.ok(response); } @@ -59,7 +61,7 @@ public ResponseEntity sendMessage( @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId ) { - ChatMessageResponse response = chatRoomService.sendMessage(userId, chatRoomId, request); + ChatMessageResponse response = chatService.sendMessage(userId, chatRoomId, request); return ResponseEntity.ok(response); } } diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/CreateChatRoomRequest.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java similarity index 91% rename from src/main/java/gg/agit/konect/domain/chat/dto/CreateChatRoomRequest.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java index 4664eca5..7a0572ac 100644 --- a/src/main/java/gg/agit/konect/domain/chat/dto/CreateChatRoomRequest.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -public record CreateChatRoomRequest( +public record ChatRoomCreateRequest( @NotNull(message = "동아리 ID는 필수입니다.") @Schema(description = "동아리 ID", example = "1", requiredMode = REQUIRED) Integer clubId diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java index 003d8096..efe53473 100644 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonFormat; @@ -36,7 +37,9 @@ public record InnerChatRoomResponse( @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) Integer unreadCount ) { - public static InnerChatRoomResponse from(ChatRoom chatRoom, User currentUser) { + public static InnerChatRoomResponse from( + ChatRoom chatRoom, User currentUser, Map unreadCountMap + ) { User chatPartner = chatRoom.getChatPartner(currentUser); return new InnerChatRoomResponse( @@ -44,15 +47,17 @@ public static InnerChatRoomResponse from(ChatRoom chatRoom, User currentUser) { chatPartner.getName(), chatPartner.getImageUrl(), chatRoom.getLastMessageContent(), - chatRoom.getLastMessageTime(), - chatRoom.getUnreadCount(currentUser.getId()) + chatRoom.getLastMessageSentAt(), + unreadCountMap.getOrDefault(chatRoom.getId(), 0) ); } } - public static ChatRoomsResponse from(List chatRooms, User currentUser) { + public static ChatRoomsResponse from( + List chatRooms, User currentUser, Map unreadCountMap + ) { return new ChatRoomsResponse(chatRooms.stream() - .map(chatRoom -> InnerChatRoomResponse.from(chatRoom, currentUser)) + .map(chatRoom -> InnerChatRoomResponse.from(chatRoom, currentUser, unreadCountMap)) .toList()); } } diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java new file mode 100644 index 00000000..1fff130d --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java @@ -0,0 +1,8 @@ +package gg.agit.konect.domain.chat.dto; + +public record UnreadMessageCount( + Integer chatRoomId, + Long unreadCount +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java index ef18625e..b1c8a4b7 100644 --- a/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java @@ -76,7 +76,7 @@ public static ChatMessage of(ChatRoom chatRoom, User sender, User receiver, Stri .build(); } - public Boolean isSentBy(Integer userId) { + public boolean isSentBy(Integer userId) { return sender.getId().equals(userId); } diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java index e89782c9..ff61c5aa 100644 --- a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java @@ -1,15 +1,15 @@ package gg.agit.konect.domain.chat.model; +import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; +import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.global.exception.CustomException; import gg.agit.konect.global.model.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,7 +17,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.Builder; import lombok.Getter; @@ -34,6 +33,12 @@ public class ChatRoom extends BaseEntity { @Column(name = "id", nullable = false, updatable = false, unique = true) private Integer id; + @Column(name = "last_message_content", columnDefinition = "TEXT") + private String lastMessageContent; + + @Column(name = "last_message_sent_at") + private LocalDateTime lastMessageSentAt; + @ManyToOne(fetch = LAZY) @JoinColumn(name = "sender_id") private User sender; @@ -42,9 +47,6 @@ public class ChatRoom extends BaseEntity { @JoinColumn(name = "receiver_id") private User receiver; - @OneToMany(mappedBy = "chatRoom", fetch = LAZY) - private List chatMessages = new ArrayList<>(); - @Builder private ChatRoom(Integer id, User sender, User receiver) { this.id = id; @@ -53,40 +55,35 @@ private ChatRoom(Integer id, User sender, User receiver) { } public static ChatRoom of(User sender, User receiver) { + validateIsNotSameParticipant(sender, receiver); return ChatRoom.builder() .sender(sender) .receiver(receiver) .build(); } - public User getChatPartner(User currentUser) { - return sender.getId().equals(currentUser.getId()) ? receiver : sender; + public static void validateIsNotSameParticipant(User sender, User receiver) { + if (sender.getId().equals(receiver.getId())) { + throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); + } } - public boolean isParticipant(Integer userId) { - return sender.getId().equals(userId) || receiver.getId().equals(userId); + public void validateIsParticipant(Integer userId) { + if (!isParticipant(userId)) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } } - public ChatMessage getLastMessage() { - return chatMessages.stream() - .max(Comparator.comparing(BaseEntity::getCreatedAt)) - .orElse(null); - } - - public String getLastMessageContent() { - ChatMessage lastMessage = getLastMessage(); - return lastMessage != null ? lastMessage.getContent() : null; + public boolean isParticipant(Integer userId) { + return sender.getId().equals(userId) || receiver.getId().equals(userId); } - public LocalDateTime getLastMessageTime() { - ChatMessage lastMessage = getLastMessage(); - return lastMessage != null ? lastMessage.getCreatedAt() : null; + public User getChatPartner(User currentUser) { + return sender.getId().equals(currentUser.getId()) ? receiver : sender; } - public Integer getUnreadCount(Integer userId) { - return (int)chatMessages.stream() - .filter(message -> message.getReceiver().getId().equals(userId)) - .filter(message -> !message.getIsRead()) - .count(); + public void updateLastMessage(String lastMessageContent, LocalDateTime lastMessageSentAt) { + this.lastMessageContent = lastMessageContent; + this.lastMessageSentAt = lastMessageSentAt; } } diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java index 570b59c8..b8116ccb 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java @@ -9,12 +9,29 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.model.ChatMessage; public interface ChatMessageRepository extends Repository { ChatMessage save(ChatMessage chatMessage); + @Query(""" + SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount( + cm.chatRoom.id, + COUNT(cm) + ) + FROM ChatMessage cm + WHERE cm.chatRoom.id IN :chatRoomIds + AND cm.receiver.id = :receiverId + AND cm.isRead = false + GROUP BY cm.chatRoom.id + """) + List countUnreadMessagesByChatRoomIdsAndUserId( + @Param("chatRoomIds") List chatRoomIds, + @Param("receiverId") Integer receiverId + ); + @Query(""" SELECT cm FROM ChatMessage cm @@ -31,7 +48,7 @@ public interface ChatMessageRepository extends Repository AND cm.receiver.id = :receiverId AND cm.isRead = false """) - List findUnreadMessages( + List findUnreadMessagesByChatRoomIdAndUserId( @Param("chatRoomId") Integer chatRoomId, @Param("receiverId") Integer receiverId ); diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java index d46b067b..5bec2da4 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java @@ -19,9 +19,8 @@ public interface ChatRoomRepository extends Repository { FROM ChatRoom cr JOIN FETCH cr.sender JOIN FETCH cr.receiver - LEFT JOIN FETCH cr.chatMessages WHERE cr.sender.id = :userId OR cr.receiver.id = :userId - ORDER BY cr.updatedAt DESC + ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id """) List findByUserId(@Param("userId") Integer userId); diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java similarity index 63% rename from src/main/java/gg/agit/konect/domain/chat/service/ChatRoomService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 4001ed75..fbb5ce4a 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -1,10 +1,10 @@ package gg.agit.konect.domain.chat.service; -import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; -import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CLUB_PRESIDENT; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -14,9 +14,10 @@ import gg.agit.konect.domain.chat.dto.ChatMessageResponse; import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.dto.CreateChatRoomRequest; +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.repository.ChatMessageRepository; @@ -31,7 +32,7 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class ChatRoomService { +public class ChatService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; @@ -39,19 +40,16 @@ public class ChatRoomService { private final ClubMemberRepository clubMemberRepository; @Transactional - public ChatRoomResponse createOrGetChatRoom(Integer userId, CreateChatRoomRequest request) { - ClubMember president = clubMemberRepository.findPresidentByClubId(request.clubId()) + public ChatRoomResponse createOrGetChatRoom(Integer userId, ChatRoomCreateRequest request) { + ClubMember clubPresident = clubMemberRepository.findPresidentByClubId(request.clubId()) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_PRESIDENT)); User currentUser = userRepository.getById(userId); - User presidentUser = president.getUser(); - if (currentUser.getId().equals(presidentUser.getId())) { - throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); - } + User president = clubPresident.getUser(); - ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), presidentUser.getId()) + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), president.getId()) .orElseGet(() -> { - ChatRoom newChatRoom = ChatRoom.of(currentUser, presidentUser); + ChatRoom newChatRoom = ChatRoom.of(currentUser, president); return chatRoomRepository.save(newChatRoom); }); @@ -60,18 +58,40 @@ public ChatRoomResponse createOrGetChatRoom(Integer userId, CreateChatRoomReques public ChatRoomsResponse getChatRooms(Integer userId) { User user = userRepository.getById(userId); + List chatRooms = chatRoomRepository.findByUserId(userId); - return ChatRoomsResponse.from(chatRooms, user); + List chatRoomIds = chatRooms.stream() + .map(ChatRoom::getId) + .toList(); + Map unreadCountMap = getUnreadCountMap(chatRoomIds, userId); + + return ChatRoomsResponse.from(chatRooms, user, unreadCountMap); + } + + private Map getUnreadCountMap(List chatRoomIds, Integer userId) { + if (chatRoomIds.isEmpty()) { + return Map.of(); + } + + List unreadMessageCounts = chatMessageRepository.countUnreadMessagesByChatRoomIdsAndUserId( + chatRoomIds, userId + ); + + return unreadMessageCounts.stream() + .collect(Collectors.toMap( + UnreadMessageCount::chatRoomId, + unreadMessageCount -> unreadMessageCount.unreadCount().intValue() + )); } @Transactional public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, Integer page, Integer limit) { ChatRoom chatRoom = chatRoomRepository.getById(roomId); - if (!chatRoom.isParticipant(userId)) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } + chatRoom.validateIsParticipant(userId); - List unreadMessages = chatMessageRepository.findUnreadMessages(roomId, userId); + List unreadMessages = chatMessageRepository.findUnreadMessagesByChatRoomIdAndUserId( + roomId, userId + ); unreadMessages.forEach(ChatMessage::markAsRead); PageRequest pageable = PageRequest.of(page - 1, limit); @@ -82,15 +102,15 @@ public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, @Transactional public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { ChatRoom chatRoom = chatRoomRepository.getById(roomId); - if (!chatRoom.isParticipant(userId)) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } + chatRoom.validateIsParticipant(userId); User sender = userRepository.getById(userId); User receiver = chatRoom.getChatPartner(sender); - ChatMessage message = ChatMessage.of(chatRoom, sender, receiver, request.content()); - ChatMessage savedMessage = chatMessageRepository.save(message); - return ChatMessageResponse.from(savedMessage, userId); + ChatMessage chatMessage = chatMessageRepository.save( + ChatMessage.of(chatRoom, sender, receiver, request.content()) + ); + chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); + return ChatMessageResponse.from(chatMessage, userId); } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 79f38f73..6fef261a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -229,11 +229,13 @@ CREATE TABLE university_schedule CREATE TABLE chat_room ( - id INT AUTO_INCREMENT PRIMARY KEY, - sender_id INT, - receiver_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + id INT AUTO_INCREMENT PRIMARY KEY, + sender_id INT, + receiver_id INT, + last_message_content TEXT NULL, + last_message_sent_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, FOREIGN KEY (sender_id) REFERENCES users (id) ON DELETE SET NULL, FOREIGN KEY (receiver_id) REFERENCES users (id) ON DELETE SET NULL