Skip to content
Merged
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.RealMatch.chat.presentation.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeHandler;

@Configuration
@EnableWebSocketMessageBroker
public class ChatWebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Value("${cors.allowed-origin}")
private String allowedOrigin;
private final ObjectProvider<HandshakeHandler> handshakeHandlerProvider;

public ChatWebSocketConfig(ObjectProvider<HandshakeHandler> handshakeHandlerProvider) {
this.handshakeHandlerProvider = handshakeHandlerProvider;
}

@Override
public void registerStompEndpoints(@NonNull StompEndpointRegistry registry) {
var registration = registry.addEndpoint("/ws/chat")
.setAllowedOrigins(allowedOrigin);
HandshakeHandler handshakeHandler = handshakeHandlerProvider.getIfAvailable();
if (handshakeHandler != null) {
registration.setHandshakeHandler(handshakeHandler);
}
}

@Override
public void configureMessageBroker(@NonNull MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
registry.enableSimpleBroker("/topic", "/queue");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.RealMatch.chat.presentation.controller;

import java.util.Objects;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;

import com.example.RealMatch.chat.presentation.controller.fixture.ChatSocketFixtureFactory;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatMessageCreatedEvent;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageAck;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageCommand;

import jakarta.validation.Valid;

@Controller
public class ChatSocketController {

private final SimpMessagingTemplate messagingTemplate;

public ChatSocketController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

@MessageMapping("/chat.send")
@SendToUser("/queue/chat.ack")
public ChatSendMessageAck sendMessage(@Valid @Payload ChatSendMessageCommand command) {
ChatMessageCreatedEvent event = ChatSocketFixtureFactory.sampleMessageCreatedEvent(command);
messagingTemplate.convertAndSend("/topic/rooms/" + command.roomId(), Objects.requireNonNull(event));
return ChatSocketFixtureFactory.sampleAck(command, event.message().messageId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.RealMatch.chat.presentation.controller.fixture;

import java.time.LocalDateTime;

import com.example.RealMatch.chat.presentation.dto.enums.ChatAttachmentStatus;
import com.example.RealMatch.chat.presentation.dto.enums.ChatAttachmentType;
import com.example.RealMatch.chat.presentation.dto.enums.ChatMessageType;
import com.example.RealMatch.chat.presentation.dto.enums.ChatSendMessageAckStatus;
import com.example.RealMatch.chat.presentation.dto.enums.ChatSenderType;
import com.example.RealMatch.chat.presentation.dto.response.ChatAttachmentInfoResponse;
import com.example.RealMatch.chat.presentation.dto.response.ChatMessageResponse;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatMessageCreatedEvent;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageAck;
import com.example.RealMatch.chat.presentation.dto.websocket.ChatSendMessageCommand;

public final class ChatSocketFixtureFactory {

private ChatSocketFixtureFactory() {
}

public static ChatMessageCreatedEvent sampleMessageCreatedEvent(ChatSendMessageCommand command) {
ChatAttachmentInfoResponse attachment = sampleAttachment(command);
String content = command.messageType() == ChatMessageType.TEXT ? command.content() : null;

ChatMessageResponse message = new ChatMessageResponse(
8001L,
command.roomId(),
202L,
ChatSenderType.USER,
command.messageType(),
content,
attachment,
null,
LocalDateTime.of(2025, 1, 1, 10, 2),
command.clientMessageId()
);
return new ChatMessageCreatedEvent(command.roomId(), message);
}

public static ChatSendMessageAck sampleAck(ChatSendMessageCommand command, Long messageId) {
return new ChatSendMessageAck(
command.clientMessageId(),
messageId,
ChatSendMessageAckStatus.SUCCESS
);
}

private static ChatAttachmentInfoResponse sampleAttachment(ChatSendMessageCommand command) {
ChatMessageType messageType = command.messageType();
if (messageType != ChatMessageType.IMAGE && messageType != ChatMessageType.FILE) {
return null;
}
ChatAttachmentType attachmentType = messageType == ChatMessageType.IMAGE
? ChatAttachmentType.IMAGE
: ChatAttachmentType.FILE;
return new ChatAttachmentInfoResponse(
command.attachmentId(),
attachmentType,
messageType == ChatMessageType.IMAGE ? "image/png" : "application/octet-stream",
messageType == ChatMessageType.IMAGE ? "photo.png" : "file.bin",
204800L,
"https://cdn.example.com/attachments/8001",
ChatAttachmentStatus.READY
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.RealMatch.chat.presentation.dto.enums;

public enum ChatSendMessageAckStatus {
SUCCESS,
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.RealMatch.chat.presentation.dto.websocket;

import com.example.RealMatch.chat.presentation.dto.response.ChatMessageResponse;

public record ChatMessageCreatedEvent(
Long roomId,
ChatMessageResponse message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.RealMatch.chat.presentation.dto.websocket;

import com.example.RealMatch.chat.presentation.dto.enums.ChatSendMessageAckStatus;

public record ChatSendMessageAck(
String clientMessageId,
Long messageId,
ChatSendMessageAckStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.RealMatch.chat.presentation.dto.websocket;

import com.example.RealMatch.chat.presentation.dto.enums.ChatMessageType;

import jakarta.validation.constraints.NotNull;

public record ChatSendMessageCommand(
@NotNull Long roomId,
@NotNull ChatMessageType messageType,
String content,
Long attachmentId,
@NotNull String clientMessageId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class SecurityConfig {
private static final String[] PERMIT_ALL_URL_ARRAY = {
"/api/test",
"/api/chat/**",
"/ws/**",
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html"
};

Expand Down Expand Up @@ -64,7 +65,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthEntr
public CorsConfigurationSource corsConfigurationSource() {

CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(allowedOrigin, "http://localhost:8080", swaggerUrl));
configuration.setAllowedOrigins(List.of(allowedOrigin, swaggerUrl));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true); // 쿠키/인증정보 포함 요청
Expand Down
Loading