졸작을 하는 중 spring을 기점으로 다중 웹 소켓 통신을 하던 과정을 나타낸 것이다.
Spring
Spring에서는 두 프레임워크의 웹 소켓 연결을 받아주고, 메세지를 보낼 때 받는 url을 설정해주어야 된다.
WebSocketConfig
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.*
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
// 엔드포인트 지정
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
}
// 메세지 주거나 받는 브로커 시작 url 설정
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry.enableSimpleBroker("/sub")
registry.setApplicationDestinationPrefixes("/pub")
}
}
ChatMessageDTO
data class ChatMessage(
val type: MessageType,
val roomId: String,
val sender: String,
val message: Map<String, Any>,
val people : PeopleType,
) {
// 메시지 타입 : 입장, 채팅
enum class MessageType {
ENTER,
TALK,
EXIT,
}
// 권한 구분
enum class PeopleType {
PARENT,
KID
}
}
Service
fun messageHandler(chatMessage: ChatMessage): MutableList<MessageResponseDTO> {
var result: MutableList<MessageResponseDTO> = mutableListOf()
when (chatMessage.type) {
ChatMessage.MessageType.ENTER -> {
userList.add(chatMessage.sender)
if (!messageBuffer.containsKey(chatMessage.roomId)) {
messageBuffer[chatMessage.roomId] = mutableListOf()
return mutableListOf()
} else {
val senderMap = mutableMapOf<String, Any>()
if (chatMessage.people == ChatMessage.PeopleType.PARENT) {
userList.remove(chatMessage.sender)
}
senderMap["list"] = userList
result.add(
MessageResponseDTO(
sender = chatMessage.sender,
message = senderMap
)
)
return result
}
}
ChatMessage.MessageType.TALK -> {
//실시간 데이터를 받을 때 데이터의 양을 조절하기 위함함
if (messageBuffer[chatMessage.roomId]?.size!! > 5000) {
messageBuffer[chatMessage.roomId]?.clear()
}
if (chatMessage.people == ChatMessage.PeopleType.PARENT) {
return agencyEnterUser(chatMessage)
}
}
ChatMessage.MessageType.EXIT -> {
// 종료 시 버퍼 제거 및 목록에서 제외
val iterator = messageBuffer[chatMessage.roomId]?.iterator()
while (iterator?.hasNext() == true) {
val message = iterator.next()
if (message.sender == chatMessage.sender) {
iterator.remove()
}
}
userList.remove(chatMessage.sender)
val senderMap = mutableMapOf<String, Any>()
senderMap["list"] = userList
result.add(
MessageResponseDTO(
sender = chatMessage.sender,
message = senderMap
)
)
return result
}
}
val message = MessageResponseDTO(sender = chatMessage.sender, message = chatMessage.message)
result.add(message)
messageBuffer[chatMessage.roomId]?.add(message)
return result
}
Unity
유니티에서는 StompHelper 플러그인을 제공하여 연결을 지원하고있다.
우리 작품에서는 Unity에서 데이터를 보내는 역할만 한다.
React
React는 실시간으로 받은 데이터를 웹에 표현하는 역할을 한다.
여기서 일대 다 기능으로 해당 유저의 데이터만 받기 위해 많은 고민을 했다.
Socket.js
import React, { useEffect, useState, useRef } from 'react';
import { Client } from "@stomp/stompjs";
import UserRealDetail from "./UserRealDetail";
const Socket = () => {
const client = useRef(null);
const [user, setUser] = useState([])
const [selectedUser, setSelectedUser] = useState(null);
const [showButtons, setShowButtons] = useState(true);
useEffect(() => {
client.current = new Client({
brokerURL: "ws://localhost:8080/ws", // 서버 WebSocket URL reconnectDelay: 5000,
onConnect: () => {
console.log("connect")
client.current.subscribe(`/sub/enter/chat/room/` + localStorage.getItem("roomId"), (message) => {
const msg = JSON.parse(message.body);
console.log(msg[0].message.list)
setUser(msg[0].message.list)
});
handleConnectionChange();
},
});
client.current.activate();
}, []);
const handleConnectionChange = () => {
publishMessage();
};
const publishMessage = () => {
if (client.current.connected) {
console.log("publish");
client.current.publish({
destination: "/pub/chat/message",
body: JSON.stringify({
type: "ENTER",
roomId: localStorage.getItem("roomId"),
sender: localStorage.getItem("agencyId"),
message: {
x: 1,
y: 1,
},
people: "PARENT",
}),
});
}
};
useEffect(() => {
if(showButtons){
publishMessage()
}
}, [showButtons])
const handleUserClick = (userName) => {
setSelectedUser(userName);
setUser(prevUsers => prevUsers.filter(user => user !== userName));
setShowButtons(false)
};
return (
<div>
{showButtons && user.map((userName, index) => (
<button key={index} onClick={() => handleUserClick(userName)}>{userName}</button>
))}
{selectedUser && !showButtons && <UserRealDetail client={client} userName={selectedUser} showButton={setShowButtons}/>}
</div>
);
}
export default Socket;
먼저 입장 시 소켓 연결을 한 후 내가 들어왔다는 사실을 알리고, 그에 따라 Unity측에서 들어온 유저들의 리스트를 확인하여 표현한다.
그 후 들어온 유저의 버튼을 클릭 시 해당 유저의 데이터만 받아올 수 있도록 subscirbe를 세팅하도록 설정하였다.
UserRealDetail.js
import React, {useEffect, useRef, useState} from "react";
import logo from "../../assets/image/login/logo.png";
import {CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis} from "recharts";
const UserRealDetail = ({client, userName, showButton}) => {
const [message, setMessage] = useState([
{
x : 0,
y : 0
}
])
const canvasRef = useRef(null);
const [count, setCount] = useState(0);
const [showChart, setShowChart] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
// 이미지 불러오기
const img = new Image();
img.onload = () => {
// 캔버스 크기 설정
canvas.width = 1920;
canvas.height = 3240;
// 이미지 그리기
ctx.drawImage(img, 0, 0);
const rect = canvas.getBoundingClientRect();
const canvasX = rect.left;
const canvasY = rect.top;
// 마커 그리기
ctx.fillStyle = 'red';
ctx.beginPath();
//const imageX = message[count].x - canvasX;
//const imageY = message[count].y - canvasY; message.forEach((point) => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fill();
});
setCount(count + 1)
};
img.src = logo;
}, [message, canvasRef]);
useEffect(() => {
if(!client.current.connected){
client.current.activate()
}
client.current.subscribe(`/sub/chat/room/` + localStorage.getItem("roomId") + "/" + userName, (message) => {
const msg = JSON.parse(message.body);
if(msg.length > 2){
setMessage(prevMessage => {
const updatedMessage = msg.map(item => ({ x: item.message.x, y: item.message.y }));
console.log(updatedMessage)
return updatedMessage;
});
return }
const x = msg[0].message.x;
const y = msg[0].message.y;
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
//const newX = x - rect.left;
//const newY = y - rect.top; setMessage(prevMessage => {
const updatedMessage = [...prevMessage, { x: x, y: y }];
return updatedMessage;
});
});
client.current.publish({
destination: "/pub/chat/message",
body: JSON.stringify({
type: "TALK",
roomId: localStorage.getItem("roomId"),
sender: userName,
message: {
x: 1,
y: 1,
},
people: "PARENT",
}),
});
}, [])
const handleButtonClick = () => {
setShowChart(!showChart);
};
const handleBackButtonClick = () => {
showButton(true)
}
return(
<div>
<button onClick={handleBackButtonClick}>돌아가기</button>
<canvas ref={canvasRef} />
<button onClick={handleButtonClick}>Show Chart</button>
{showChart && (
<LineChart width={800} height={400} data={message}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis /> <Tooltip /> <Legend /> <Line type="monotone" dataKey="x" stroke="#8884d8" />
<Line type="monotone" dataKey="y" stroke="#8884d8" />
</LineChart> )}
</div>)
}
export default UserRealDetail
이 부분에서 혹시 Unity에서 컨텐츠를 하고 있는 유저의 데이터를 Spring에서 저장하고 있다가, React에서 뒤늦게 해당 유저의 실시간 정보를 확인할 때 버퍼에 해당 유저의 정보를 따로 요청하여 가져올 수 있도록 하였다.
원래는 버퍼를 Radis를 사용하고 싶었지만..... 기회가 된다면 시도해보겠다..
'개발일지' 카테고리의 다른 글
| 실시간 화면 공유 플랫폼 만들기 With kurento media server (0) | 2024.07.19 |
|---|---|
| 첫글 (2) | 2023.01.19 |