Skip to content

Commit

Permalink
Merge branch 'dev' into test/#89-daily-check
Browse files Browse the repository at this point in the history
  • Loading branch information
Minjae-An committed May 26, 2024
2 parents c421f36 + 0b53e82 commit b15ba02
Show file tree
Hide file tree
Showing 25 changed files with 432 additions and 61 deletions.
8 changes: 8 additions & 0 deletions be/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ dependencies {
// fcm
implementation 'com.google.firebase:firebase-admin:9.2.0'

// websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'org.webjars:sockjs-client:1.1.2'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// h2
testRuntimeOnly 'com.h2database:h2'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,63 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import yeonba.be.chatting.dto.request.ChatPublishRequest;
import yeonba.be.chatting.dto.response.ChatMessageResponse;
import yeonba.be.chatting.dto.response.ChatRoomResponse;
import yeonba.be.chatting.service.ChatService;
import yeonba.be.util.CustomResponse;

@Tag(name = "Chatting", description = "채팅 API")
@RestController
@Slf4j
@Controller
@RequiredArgsConstructor
public class ChattingController {
public class ChatController {

private final ChatService chatService;

@Operation(summary = "채팅 목록 조회", description = "자신이 참여 중인 채팅 목록을 조회할 수 있습니다.")
@MessageMapping("/chat")
public void chat(ChatPublishRequest request) {

log.info("chatting test log {}", request.getContent());

chatService.publish(request);
}

@Operation(summary = "채팅 메시지 목록 조회", description = "특정 채팅방의 메시지 목록을 조회할 수 있습니다.")
@ApiResponse(responseCode = "200", description = "채팅 메시지 목록 조회 성공")
@ResponseBody
@GetMapping("/chat-rooms/{roomId}/messages")
public ResponseEntity<CustomResponse<List<ChatMessageResponse>>> getChatMessages(
@RequestAttribute("userId") long userId,
@Parameter(description = "채팅방 ID", example = "1")
@PathVariable long roomId) {

List<ChatMessageResponse> response = chatService.getChatMessages(userId, roomId);

return ResponseEntity
.ok()
.body(new CustomResponse<>(response));
}

@Operation(summary = "채팅방 목록 조회", description = "자신이 참여 중인 채팅 목록을 조회할 수 있습니다.")
@ApiResponse(responseCode = "200", description = "참여 중인 채팅 목록 조회 성공")
@GetMapping("/chattings")
@ResponseBody
@GetMapping("/chat-rooms")
public ResponseEntity<CustomResponse<List<ChatRoomResponse>>> getChatRooms(
@RequestAttribute("userId") long userId) {

Expand All @@ -38,6 +74,7 @@ public ResponseEntity<CustomResponse<List<ChatRoomResponse>>> getChatRooms(

@Operation(summary = "채팅 요청", description = "다른 사용자에게 채팅을 요청할 수 있습니다.")
@ApiResponse(responseCode = "200", description = "채팅 요청 정상 처리")
@ResponseBody
@PostMapping("/users/{partnerId}/chat")
public ResponseEntity<CustomResponse<Void>> requestChat(
@RequestAttribute("userId") long userId,
Expand All @@ -53,6 +90,7 @@ public ResponseEntity<CustomResponse<Void>> requestChat(

@Operation(summary = "채팅 요청 수락", description = "요청받은 채팅을 수락할 수 있습니다.")
@ApiResponse(responseCode = "200", description = "채팅 요청 수락 정상 처리")
@ResponseBody
@PostMapping("/notifications/{notificationId}/chat")
public ResponseEntity<CustomResponse<Void>> acceptRequestedChat(
@RequestAttribute("userId") long userId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package yeonba.be.chatting.dto.request;

import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;

@Getter
public class ChatPublishRequest implements Serializable {

private long roomId;
private long userId;
private String userName;
private String content;
private LocalDateTime sentAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package yeonba.be.chatting.dto.request;

import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;

@Getter
public class ChatSubscribeResponse implements Serializable {

private long roomId;
private long userId;
private String userName;
private String content;
private LocalDateTime sentAt;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package yeonba.be.chatting.dto.response;

import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ChatMessageResponse {

private long userId;
private String userName;
private String content;
private LocalDateTime sentAt;
}
3 changes: 2 additions & 1 deletion be/src/main/java/yeonba/be/chatting/entity/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ public class ChatMessage {

private LocalDateTime deletedAt;

public ChatMessage(ChatRoom chatRoom, User sender, User receiver, String content) {
public ChatMessage(ChatRoom chatRoom, User sender, User receiver, String content, LocalDateTime sentAt) {

this.chatRoom = chatRoom;
this.sender = sender;
this.receiver = receiver;
this.content = content;
this.sentAt = sentAt;
this.read = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class ChatMessageCommand {

private final ChatMessageRepository chatMessageRepository;

public ChatMessage createChatMessage(ChatMessage message) {
public ChatMessage save(ChatMessage message) {

return chatMessageRepository.save(message);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package yeonba.be.chatting.repository.chatmessage;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import yeonba.be.chatting.entity.ChatMessage;
import yeonba.be.chatting.entity.ChatRoom;

@Component
@RequiredArgsConstructor
Expand All @@ -19,4 +21,9 @@ public int countUnreadMessagesByChatRoomId(long chatRoomId) {

return chatMessageRepository.countByChatRoomIdAndReadIsFalse(chatRoomId);
}

public List<ChatMessage> findAllByChatRoom(ChatRoom chatRoom) {

return chatMessageRepository.findAllByChatRoomOrderBySentAtDesc(chatRoom);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package yeonba.be.chatting.repository.chatmessage;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import yeonba.be.chatting.entity.ChatMessage;
import yeonba.be.chatting.entity.ChatRoom;

@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {

ChatMessage findFirstByChatRoomIdOrderBySentAtDesc(long chatRoomId);

int countByChatRoomIdAndReadIsFalse(long chatRoomId);

List<ChatMessage> findAllByChatRoomOrderBySentAtDesc(ChatRoom chatRoom);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public class ChatRoomQuery {

private final ChatRoomRepository chatRoomRepository;

public ChatRoom findById(long id) {

return chatRoomRepository.findById(id)
.orElseThrow(() -> new GeneralException(NOT_FOUND_CHAT_ROOM));
}

public List<ChatRoom> findAllBy(User user) {

return chatRoomRepository.findAllByUserAndActiveIsTrue(user);
Expand Down
64 changes: 59 additions & 5 deletions be/src/main/java/yeonba/be/chatting/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import yeonba.be.chatting.dto.request.ChatPublishRequest;
import yeonba.be.chatting.dto.response.ChatMessageResponse;
import yeonba.be.chatting.dto.response.ChatRoomResponse;
import yeonba.be.chatting.entity.ChatMessage;
import yeonba.be.chatting.entity.ChatRoom;
import yeonba.be.chatting.repository.chatmessage.ChatMessageCommand;
import yeonba.be.chatting.repository.chatmessage.ChatMessageQuery;
import yeonba.be.chatting.repository.chatroom.ChatRoomCommand;
import yeonba.be.chatting.repository.chatroom.ChatRoomQuery;
import yeonba.be.chatting.repository.chatroom.ChatRoomRepository;
import yeonba.be.exception.BlockException;
import yeonba.be.exception.ChatException;
import yeonba.be.exception.GeneralException;
import yeonba.be.exception.NotificationException;
import yeonba.be.notification.entity.Notification;
Expand All @@ -26,6 +33,7 @@
import yeonba.be.user.repository.block.BlockQuery;
import yeonba.be.user.repository.user.UserQuery;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatService {
Expand All @@ -36,9 +44,47 @@ public class ChatService {
private final ChatMessageQuery chatMessageQuery;
private final UserQuery userQuery;
private final BlockQuery blockQuery;
private final NotificationQuery notificationQuey;
private final NotificationQuery notificationQuery;

private final ApplicationEventPublisher eventPublisher;
private final RedisChattingPublisher redisChattingPublisher;
private final RedisChattingSubscriber adapter;
private final RedisMessageListenerContainer container;

@Transactional
public void publish(ChatPublishRequest request) {

ChatRoom chatRoom = chatRoomQuery.findById(request.getRoomId());
User sender = userQuery.findById(request.getUserId());
User receiver = chatRoom.getSender().equals(sender) ? chatRoom.getReceiver()
: chatRoom.getSender();

// TODO: 메시지 Pub/Sub과 메시지 저장 로직 비동기 처리(id, user 등 request, response 변경 가능)
redisChattingPublisher.publish(new ChannelTopic(String.valueOf(request.getRoomId())),
request);
chatMessageCommand.save(
new ChatMessage(chatRoom, sender, receiver, request.getContent(), request.getSentAt()));
}

@Transactional(readOnly = true)
public List<ChatMessageResponse> getChatMessages(long userId, long roomId) {

User user = userQuery.findById(userId);

ChatRoom chatRoom = chatRoomQuery.findById(roomId);

if (!user.equals(chatRoom.getSender()) && !user.equals(chatRoom.getReceiver())) {
throw new GeneralException(ChatException.NOT_YOUR_CHAT_ROOM);
}

List<ChatMessage> chatMessages = chatMessageQuery.findAllByChatRoom(chatRoom);

return chatMessages.stream()
.map(chatMessage -> new ChatMessageResponse(chatMessage.getSender().getId(),
chatMessage.getSender().getNickname(),
chatMessage.getContent(), chatMessage.getSentAt()))
.toList();
}

@Transactional(readOnly = true)
public List<ChatRoomResponse> getChatRooms(long userId) {
Expand Down Expand Up @@ -89,9 +135,10 @@ public void requestChat(long senderId, long receiverId) {
eventPublisher.publishEvent(notificationSendEvent);
}

@Transactional
public void acceptRequestedChat(long userId, long notificationId) {

Notification notification = notificationQuey.findById(notificationId);
Notification notification = notificationQuery.findById(notificationId);

// 채팅 요청 알림인지 검증
if (!notification.getType().isChattingRequest()) {
Expand All @@ -105,15 +152,22 @@ public void acceptRequestedChat(long userId, long notificationId) {
// 본인에게 온 채팅 요청인지 검증
if (receiver.equals(userQuery.findById(userId))) {

throw new GeneralException(NotificationException.NOT_YOUR_CHATTING_REQUEST_NOTIFICATION);
throw new GeneralException(
NotificationException.NOT_YOUR_CHATTING_REQUEST_NOTIFICATION);
}

// 채팅방 활성화
ChatRoom chatRoom = chatRoomQuery.findBy(sender, receiver);
chatRoom.activeRoom();

String activeRoom = "채팅방이 활성화되었습니다.";
chatMessageCommand.createChatMessage(new ChatMessage(chatRoom, sender, receiver, activeRoom));
String activeRoom = "채팅방이 생성되었습니다.";

chatMessageCommand.save(
new ChatMessage(chatRoom, sender, receiver, activeRoom, LocalDateTime.now()));

// 메시지 수신을 위한 Redis Pub/Sub 구독
container.addMessageListener(adapter, new ChannelTopic(String.valueOf(chatRoom.getId())));
log.info("channel topic 생성 {}", chatRoom.getId());

NotificationSendEvent notificationSendEvent = new NotificationSendEvent(
NotificationType.CHATTING_REQUEST_ACCEPTED, receiver, sender,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package yeonba.be.chatting.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;
import yeonba.be.chatting.dto.request.ChatPublishRequest;

@Service
@RequiredArgsConstructor
public class RedisChattingPublisher {

private final RedisTemplate<String, Object> redisTemplate;

public void publish(ChannelTopic topic, ChatPublishRequest request) {

redisTemplate.convertAndSend(topic.getTopic(), request);
}
}
Loading

0 comments on commit b15ba02

Please sign in to comment.