-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 채팅 기능 구현 #408
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
feat: 채팅 기능 구현 #408
Changes from all commits
6588e2a
5135613
e1f20d6
449049f
14e4c6c
6948995
541747a
67507b1
cdadb15
be5240a
a437240
5ea6eaf
a7df7bb
df2b913
c6b5115
a89dd5f
fa58816
8ea1cca
fe46db3
747c635
320d5f5
0189452
fa1f474
3241b0b
2d9acb5
d9ad86b
02def7d
a255b3a
a2e8b1f
78e9ad2
8f698cb
b535f6b
82219b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.example.solidconnection.chat.controller; | ||
|
|
||
| import com.example.solidconnection.chat.dto.ChatMessageResponse; | ||
| import com.example.solidconnection.chat.dto.ChatRoomListResponse; | ||
| import com.example.solidconnection.chat.service.ChatService; | ||
| import com.example.solidconnection.common.dto.SliceResponse; | ||
| import com.example.solidconnection.common.resolver.AuthorizedUser; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.data.web.PageableDefault; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PutMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/chats") | ||
| public class ChatController { | ||
|
|
||
| private final ChatService chatService; | ||
|
|
||
| @GetMapping("/rooms") | ||
| public ResponseEntity<ChatRoomListResponse> getChatRooms( | ||
| @AuthorizedUser long siteUserId | ||
| ) { | ||
| ChatRoomListResponse chatRoomListResponse = chatService.getChatRooms(siteUserId); | ||
| return ResponseEntity.ok(chatRoomListResponse); | ||
| } | ||
|
|
||
| @GetMapping("/rooms/{room-id}") | ||
| public ResponseEntity<SliceResponse<ChatMessageResponse>> getChatMessages( | ||
| @AuthorizedUser long siteUserId, | ||
| @PathVariable("room-id") Long roomId, | ||
| @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable | ||
| ) { | ||
| SliceResponse<ChatMessageResponse> response = chatService.getChatMessages(siteUserId, roomId, pageable); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @PutMapping("/rooms/{room-id}/read") | ||
| public ResponseEntity<Void> markChatMessagesAsRead( | ||
| @AuthorizedUser long siteUserId, | ||
| @PathVariable("room-id") Long roomId | ||
| ) { | ||
| chatService.markChatMessagesAsRead(siteUserId, roomId); | ||
| return ResponseEntity.ok().build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.example.solidconnection.chat.dto; | ||
|
|
||
| import java.time.ZonedDateTime; | ||
|
|
||
| public record ChatAttachmentResponse( | ||
| long id, | ||
| boolean isImage, | ||
| String url, | ||
| String thumbnailUrl, | ||
| ZonedDateTime createdAt | ||
| ) { | ||
|
|
||
| public static ChatAttachmentResponse of(long id, boolean isImage, String url, | ||
| String thumbnailUrl, ZonedDateTime createdAt) { | ||
| return new ChatAttachmentResponse(id, isImage, url, thumbnailUrl, createdAt); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.example.solidconnection.chat.dto; | ||
|
|
||
| import java.time.ZonedDateTime; | ||
| import java.util.List; | ||
|
|
||
| public record ChatMessageResponse( | ||
| long id, | ||
| String content, | ||
| long senderId, | ||
| ZonedDateTime createdAt, | ||
| List<ChatAttachmentResponse> attachments | ||
| ) { | ||
|
|
||
| public static ChatMessageResponse of(long id, String content, long senderId, | ||
| ZonedDateTime createdAt, List<ChatAttachmentResponse> attachments) { | ||
| return new ChatMessageResponse(id, content, senderId, createdAt, attachments); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.solidconnection.chat.dto; | ||
|
|
||
| public record ChatParticipantResponse( | ||
| long partnerId, | ||
| String nickname, | ||
| String profileUrl | ||
| ) { | ||
|
|
||
| public static ChatParticipantResponse of(long partnerId, String nickname, String profileUrl) { | ||
| return new ChatParticipantResponse(partnerId, nickname, profileUrl); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.solidconnection.chat.dto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record ChatRoomListResponse( | ||
| List<ChatRoomResponse> chatRooms | ||
| ) { | ||
|
|
||
| public static ChatRoomListResponse of(List<ChatRoomResponse> chatRooms) { | ||
| return new ChatRoomListResponse(chatRooms); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.example.solidconnection.chat.dto; | ||
|
|
||
| import java.time.ZonedDateTime; | ||
|
|
||
| public record ChatRoomResponse( | ||
| long id, | ||
| String lastChatMessage, | ||
| ZonedDateTime lastReceivedTime, | ||
| ChatParticipantResponse partner, | ||
| long unReadCount | ||
| ) { | ||
|
|
||
| public static ChatRoomResponse of( | ||
| long id, | ||
| String lastChatMessage, | ||
| ZonedDateTime lastReceivedTime, | ||
| ChatParticipantResponse partner, | ||
| long unReadCount | ||
| ) { | ||
| return new ChatRoomResponse(id, lastChatMessage, lastReceivedTime, partner, unReadCount); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.example.solidconnection.chat.repository; | ||
|
|
||
| import com.example.solidconnection.chat.domain.ChatAttachment; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ChatAttachmentRepository extends JpaRepository<ChatAttachment, Long> { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.example.solidconnection.chat.repository; | ||
|
|
||
| import com.example.solidconnection.chat.domain.ChatMessage; | ||
| import java.util.Optional; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> { | ||
|
|
||
| @Query(""" | ||
| SELECT cm FROM ChatMessage cm | ||
| LEFT JOIN FETCH cm.chatAttachments | ||
| WHERE cm.chatRoom.id = :roomId | ||
| ORDER BY cm.createdAt DESC | ||
| """) | ||
| Slice<ChatMessage> findByRoomIdWithPaging(@Param("roomId") long roomId, Pageable pageable); | ||
|
Comment on lines
+13
to
+19
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 채팅 메세지를 최신순으로 정렬하여 응답한다면, 예를 들어
라는 메세지가
이런식으로 보이지 않을까요..?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음 ... 저도 이 부분은 프론트의 영역이라고 생각합니다 ! 저희는 단순히 최신 순으로 정렬된 채팅을 전달해주면 된다고 생각합니다. |
||
|
|
||
| Optional<ChatMessage> findFirstByChatRoomIdOrderByCreatedAtDesc(long chatRoomId); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.solidconnection.chat.repository; | ||
|
|
||
| import com.example.solidconnection.chat.domain.ChatParticipant; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ChatParticipantRepository extends JpaRepository<ChatParticipant, Long> { | ||
|
|
||
| boolean existsByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId); | ||
|
|
||
| Optional<ChatParticipant> findByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.example.solidconnection.chat.repository; | ||
|
|
||
| import com.example.solidconnection.chat.domain.ChatReadStatus; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| public interface ChatReadStatusRepository extends JpaRepository<ChatReadStatus, Long> { | ||
|
|
||
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @Query(value = """ | ||
| INSERT INTO chat_read_status (chat_room_id, chat_participant_id, created_at, updated_at) | ||
| VALUES (:chatRoomId, :chatParticipantId, NOW(6), NOW(6)) | ||
| ON DUPLICATE KEY UPDATE updated_at = NOW(6) | ||
| """, nativeQuery = true) | ||
| void upsertReadStatus(@Param("chatRoomId") long chatRoomId, @Param("chatParticipantId") long chatParticipantId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.example.solidconnection.chat.repository; | ||
|
|
||
| import com.example.solidconnection.chat.domain.ChatRoom; | ||
| import java.util.List; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> { | ||
|
|
||
| @Query(""" | ||
| SELECT cr FROM ChatRoom cr | ||
| JOIN cr.chatParticipants cp | ||
| WHERE cp.siteUserId = :userId AND cr.isGroup = false | ||
| ORDER BY ( | ||
| SELECT MAX(cm.createdAt) | ||
| FROM ChatMessage cm | ||
| WHERE cm.chatRoom = cr | ||
| ) DESC NULLS LAST | ||
| """) | ||
| List<ChatRoom> findOneOnOneChatRoomsByUserId(@Param("userId") long userId); | ||
Gyuhyeok99 marked this conversation as resolved.
Show resolved
Hide resolved
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 조회는 Slice 로 페이지네이션 응답을 할 필요는 없나요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하! 회의때는 채팅방이 많이 개설되지 않을 것이라고 이야기를 해서 굳이 Slice를 하지 말자! 고 이야기가 나왔었는데 이 부분도 Slice를 적용할까요?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하~ 일리가 있네요, 페이지네이션 반영 안하셔도 됩니다!👍 |
||
|
|
||
| @Query(""" | ||
| SELECT COUNT(cm) FROM ChatMessage cm | ||
| LEFT JOIN ChatReadStatus crs ON crs.chatRoomId = cm.chatRoom.id | ||
| AND crs.chatParticipantId = ( | ||
| SELECT cp.id FROM ChatParticipant cp | ||
| WHERE cp.chatRoom.id = :chatRoomId | ||
| AND cp.siteUserId = :userId | ||
| ) | ||
| WHERE cm.chatRoom.id = :chatRoomId | ||
| AND cm.senderId != :userId | ||
| AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt) | ||
| """) | ||
| long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId); | ||
|
Comment on lines
+23
to
+35
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 지식 없이 코드만 보는 입장에서는, 정리해서 다시 말하자면 이렇습니다! AS-IS
TO-BE 정책상 제가 모르는 부분이 있다면 말씀해주세요 🙇🏻♀️
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 좋은 이야기 감사합니다! 리뷰 작성하면서 AND cm.senderId != :userId 필터링 조건이 있으면 성능이 더 좋은가?라는 생각도 들긴 했는데 이건 테스트를 해보지 않는 이상 잘 모르겠어서 확신이 안드네요 😅
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
다시 생각해보니 스키마 배경지식 없이, 일반적으로 생각한다면 다른 사람이 보낸 것만 카운트하는게 맞겠네요😅 |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.