Skip to content
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

[BE] feat: 채팅 알림에 모임 정보(모임 사진, 모임 이름) 추가 #706

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-amqp' // rabbitmq
implementation 'org.springframework.boot:spring-boot-starter-reactor-netty' // rabbitmq
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation "org.springframework.boot:spring-boot-starter-actuator"
Expand Down Expand Up @@ -91,12 +93,12 @@ tasks.withType(GenerateSwaggerUI) {
doFirst {
def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yaml")

def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
" - APIKey: [] # Apply the security scheme here"

swaggerUIFile.append securitySchemesContent
Expand Down Expand Up @@ -130,5 +132,5 @@ resolveMainClassName {

test {
systemProperty 'user.timezone', 'Asia/Seoul'

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
@RestController
@RequestMapping("/auth")
public class AuthController {

// TODO: KakaoMemberService와 AuthService 통합
private final KakaoMemberService kakaoMemberService;
private final AuthService authService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.happy.friendogly.chat.config;

public interface ChatTemplate {

void convertAndSend(Long chatRoomId, Object payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.happy.friendogly.chat.config;

import org.springframework.context.annotation.Profile;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
@Profile("local")
public class InMemoryChatTemplate implements ChatTemplate {

private static final String TOPIC_CHAT_PREFIX = "/exchange/chat.exchange/room.";

private final SimpMessagingTemplate template;

public InMemoryChatTemplate(SimpMessagingTemplate template) {
this.template = template;
}

@Override
public void convertAndSend(Long chatRoomId, Object payload) {
template.convertAndSend(TOPIC_CHAT_PREFIX + chatRoomId, payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.happy.friendogly.chat.config;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!local")
public class RabbitChatTemplate implements ChatTemplate {

private final RabbitTemplate rabbitTemplate;

public RabbitChatTemplate(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}

@Override
public void convertAndSend(Long chatRoomId, Object payload) {
rabbitTemplate.convertAndSend("chat.exchange", "room." + chatRoomId, payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,35 @@
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatSocketController {

private final ChatCommandService chatCommandService;
private final ChatRoomQueryService chatRoomQueryService;
private final SimpMessagingTemplate template;

public ChatSocketController(
ChatCommandService chatCommandService,
ChatRoomQueryService chatRoomQueryService,
SimpMessagingTemplate template
ChatRoomQueryService chatRoomQueryService
) {
this.chatCommandService = chatCommandService;
this.chatRoomQueryService = chatRoomQueryService;
this.template = template;
}

@MessageMapping("/invite")
@MessageMapping("/invite") // TODO: 1대1 채팅방 구현 시 수정 필요
public void invite(
@WebSocketAuth Long senderMemberId,
@Payload InviteToChatRoomRequest request
) {
chatRoomQueryService.validateInvitation(senderMemberId, request);
template.convertAndSend(
"/topic/invite/" + request.receiverMemberId(),
new InviteToChatRoomResponse(request.chatRoomId())
);
}

@Deprecated
@MessageMapping("/enter/{chatRoomId}")
public void enter(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId
) {
chatCommandService.sendEnter(memberId, chatRoomId);
// template.convertAndSend(
// "/topic/invite/" + request.receiverMemberId(),
// new InviteToChatRoomResponse(request.chatRoomId())
// );
}

@MessageMapping("/chat/{chatRoomId}")
@MessageMapping("chat.{chatRoomId}")
public void sendMessage(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId,
Expand All @@ -66,15 +53,6 @@ public void sendMessage(
chatCommandService.sendChat(memberId, chatRoomId, request);
}

@Deprecated
@MessageMapping("/leave/{chatRoomId}")
public void leave(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId
) {
chatCommandService.sendLeave(memberId, chatRoomId);
}

@MessageExceptionHandler
public ResponseEntity<ApiResponse<ErrorResponse>> handleException(FriendoglyException exception) {
ErrorResponse errorResponse = new ErrorResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,51 @@
import static com.happy.friendogly.chat.domain.MessageType.ENTER;
import static com.happy.friendogly.chat.domain.MessageType.LEAVE;

import com.happy.friendogly.chat.config.ChatTemplate;
import com.happy.friendogly.chat.domain.ChatMessage;
import com.happy.friendogly.chat.domain.ChatRoom;
import com.happy.friendogly.chat.domain.MessageType;
import com.happy.friendogly.chat.dto.request.ChatMessageSocketRequest;
import com.happy.friendogly.chat.dto.response.ChatMessageSocketResponse;
import com.happy.friendogly.chat.repository.ChatMessageRepository;
import com.happy.friendogly.chat.repository.ChatRoomRepository;
import com.happy.friendogly.club.domain.Club;
import com.happy.friendogly.club.repository.ClubRepository;
import com.happy.friendogly.exception.FriendoglyException;
import com.happy.friendogly.member.domain.Member;
import com.happy.friendogly.member.repository.MemberRepository;
import com.happy.friendogly.notification.service.NotificationService;
import java.time.LocalDateTime;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ChatCommandService {

private static final String TOPIC_CHAT_PREFIX = "/topic/chat/";
private static final String EMPTY_CONTENT = "";

private final MemberRepository memberRepository;
private final ClubRepository clubRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final NotificationService notificationService;
private final SimpMessagingTemplate template;
private final ChatTemplate chatTemplate;

public ChatCommandService(
MemberRepository memberRepository,
ClubRepository clubRepository,
ChatRoomRepository chatRoomRepository,
ChatMessageRepository chatMessageRepository,
NotificationService notificationService,
SimpMessagingTemplate template
ChatTemplate chatTemplate
) {
this.memberRepository = memberRepository;
this.clubRepository = clubRepository;
this.chatRoomRepository = chatRoomRepository;
this.chatMessageRepository = chatMessageRepository;
this.notificationService = notificationService;
this.template = template;
this.chatTemplate = chatTemplate;
}

public void sendEnter(Long senderMemberId, Long chatRoomId) {
Expand All @@ -71,8 +75,11 @@ public void sendLeave(Long senderMemberId, Long chatRoomId) {
}

private void sendAndSave(MessageType messageType, String content, ChatRoom chatRoom, Member senderMember) {
ChatMessageSocketResponse chat = new ChatMessageSocketResponse(messageType, content, senderMember, LocalDateTime.now());
notificationService.sendChatNotification(chatRoom.getId(), chat);
ChatMessageSocketResponse chat = new ChatMessageSocketResponse(
messageType, content, senderMember, LocalDateTime.now());
Club club = clubRepository.getByChatRoomId(chatRoom.getId());

notificationService.sendChatNotification(chatRoom.getId(), chat, club);
template.convertAndSend(TOPIC_CHAT_PREFIX + chatRoom.getId(), chat);
chatMessageRepository.save(new ChatMessage(chatRoom, messageType, senderMember, content));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.happy.friendogly.config;

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.rabbitmq.client.ConnectionFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@EnableRabbit
@Profile("!local")
public class RabbitMqConfig {

private static final String CHAT_QUEUE_NAME = "chat.queue";
private static final String CHAT_EXCHANGE_NAME = "chat.exchange";
private static final String ROUTING_KEY = "*.room.*";

private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

@Value("${spring.rabbitmq.host}")
private String host;

@Value("${spring.rabbitmq.username}")
private String username;

@Value("${spring.rabbitmq.password}")
private String password;

@Value("${spring.rabbitmq.stomp-port}")
private int port;

@Bean
public Queue queue() {
return new Queue(CHAT_QUEUE_NAME, true);
}

@Bean
public TopicExchange topicExchange() {
return new TopicExchange(CHAT_EXCHANGE_NAME, true, false);
}

@Bean
public Binding binding(Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue)
.to(topicExchange)
.with(ROUTING_KEY);
}

@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
return connectionFactory.getRabbitConnectionFactory();
}

@Bean
public AmqpAdmin amqpAdmin(RabbitTemplate rabbitTemplate) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
rabbitAdmin.declareExchange(topicExchange());
return rabbitAdmin;
}

@Bean
public RabbitTemplate rabbitTemplate(
org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) {

RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter());
return rabbitTemplate;
}

@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(javaTimeModule());
return new Jackson2JsonMessageConverter(objectMapper);
}

@Bean
public Module javaTimeModule() {
return new JavaTimeModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(TIME_FORMAT))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(TIME_FORMAT));
}
}
Loading
Loading