- 개발환경
- spring boot
- spring security
- maven
웹 소켓(Web Socket)이란?
HTTP와는 다른 통신 프로토콜로 웹 서버와 웹 브라우저가 서로 동시에 실시간으로 데이터를 송수신 처리할 수 있는 양방향 통신 방법
(채팅, 게임, 메타버스 등!!)
* HTTP 프로토콜과 다른 점
- 연결 유지 : 연결이 한번 수립되면 지속적으로 유지 -> 양방향 (HTTP는 요청-응답 후 연결 종료 -> 단방향)
- 실시간 통신 : 연결 유지 -> 서버가 데이터 보내면 클라이언트는 즉시 데이터 수신 가능 (HTTP는 클라이언트가 서버에 요청을 보내야 서버가 응답을 전송할 수 있음)
- 이진 데이터 전송 : 이진 데이터(텍스트, 이미지, 비디오 등 다양한 형식 가능) 전송 가능 (HTTP는 텍스트 기반)
* TCP 기반의 프로토콜
TCP : Transmission Control Protocol 데이터를 안정적으로 전송하기 위한 연결형 프로토콜
신뢰성, 연결 지향 -> 데이터를 전송하기 전에 클라이언트 - 서버 간에 논리적인 연결 수립 -> 신뢰성 있는 데이터 전송
상태를 유지하면서 연결 유지 -> 대기 시간이 줄어듬 -> 빠른 응답으로 실시간 데이터 교환
1. pom.xml에 웹 소켓 라이브러리 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. WebSocketConfig.java 클래스 생성
- /chat 이라는 요청에 대해서 ChatSocketHandler가 처리하도록 설정
package com.example.web.websocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Autowired
ChatSocketHandler chatSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
/*
* withSockJS() : SockJS 라이브러리를 이용해서 WebSocket을 지원하지 않는 브라우저에서도 WebSocket 프로토콜을 사용할 수 있도록 하는 기능
* setInterceptors(new HttpSessionHandshakeInterceptor() : WebSocket 연결을 설정할 때 HttpSession의 속성을 WebSocket 세션 속성으로 복사
* -> WebSocketSession에서 HttpSession에 저장된 데이터에 액세스할 수 있음
*/
registry.addHandler(chatSocketHandler, "/chat").withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
}
}
3. ChatMessage.java에 메시지 형태 선언
4. ChatSocketHandler.java
Security 구조를 뜯어보게 만들었던.. 정말 머리를 쥐어싸맸던 부분
로그인한 사용자 정보(id, userType)를 도대체 어느 객체에서 어떻게 가져와야 할 지를 몰라 일일이 로그를 찍어보면서 확인했다
WebSocketSession에서 Spring Security의 인증정보를 가져와서 인증된 사용자의 정보를 가져오는 코드
1) 로그인한 사용자 정보가 SecurityContext 객체의 형태로 저장되어 있는 것 확인
2) 로그인한 사용자 정보 객체가 "SPRING_SECURITY_CONTEXT"라는 이름으로 저장되어 있는 것 확인
3) 첫번째 줄에서 WebSocket 세션의 속성(attributes) 중 "SPRING_SECURITY_CONTEXT" 이름을 가진 SecurityContextlmpl 객체 획득 ( 이 속성은 Spring Security에서 인증을 처리한 후에 세션에 저장 )
4) 두번째 줄에서 가져온 SecurityContextlmpl 객체에서 인증된 Authentication 객체 획득한 뒤 CustomAuthenticationToken으로 형변환
5) 세번째 줄에서는 Token 객체에서 인증된 사용자 정보(UserDetails 인터페이스) 가 들어있는 principal 획득 후 CustomUserDetails 객체로 형변환
6) 사용자 정보(userId, userType) 획득
package com.example.web.websocket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.example.security.CustomAuthenticationToken;
import com.example.security.vo.CustomUserDetails;
import com.fasterxml.jackson.databind.ObjectMapper;
@Slf4j
@Service
public class ChatSocketHandler extends TextWebSocketHandler {
private ObjectMapper objectMapper = new ObjectMapper();
// 채팅룸(상담중인 직원, 고객 포함)을 관리하는 맵
// Collections.synchronizedMap() : map 인터페이스를 구현한 객체를 스레드가 안전한(synchronized) 맵으로 만들어주는 메소드
// (다수의 스레드에서 동시에 맵에 접근해도 안전하게 데이터 보호)
private Map<String, Map<String, WebSocketSession>> chatRooms = Collections.synchronizedMap(new HashMap<>());
// 상담대기중인 직원들을 관리하는 맵
private Map<String, WebSocketSession> waitingEmployeeSessions = Collections.synchronizedMap(new HashMap<>());
// 상담페이지에 접속한 고객들을 관리하는 맵
private Map<String, WebSocketSession> customerSessions = Collections.synchronizedMap(new HashMap<>());
// 웹 소켓 연결이 성공적으로 완료되면 실행되는 메소드- 로그인한 사용자의 정보를 확인해서 적절한 맵에 세션 저장
// 자동으로 생성되는 WebSocketSession
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
/*
* 로그인한 사용자 정보 -> CustomAuthenticationToken 객체에 들어있음
* WebSocketSession에서 세션 정보를 가져와 spring security의 SecurityContext 객체를 꺼내고, 그 안에 있는 CustomAuthenticationToken 객체 가져오기
* session.getAttributes() : WebSocketSession에서 세션 정보 가져옴 (map 형태의 세션 정보 반환)
* get("SPRING_SECURITY_CONTEXT") : "SPRING_SECURITY_CONTEXT" 속성명으로 들어있는 spring security의 SecurityContext를 가져옴
* securityContext.getAuthentication() : 인증정보 (Authentication) 객체를 가져옴 -> CustomAuthenticationToken으로 타입 변환
*/
SecurityContextImpl securityContext = (SecurityContextImpl) session.getAttributes().get("SPRING_SECURITY_CONTEXT");
CustomAuthenticationToken authenticationToken = (CustomAuthenticationToken) securityContext.getAuthentication();
CustomUserDetails userDetails = (CustomUserDetails) authenticationToken.getPrincipal();
String loginId = userDetails.getUsername();
String loginType = authenticationToken.getUserType();
if ("사용자".equals(loginType)) {
customerSessions.put(loginId, session);
} else if ("관리자".equals(loginType)) {
waitingEmployeeSessions.put(loginId, session);
}
}
// 클라이언트로부터 웹소켓으로 메시지를 수신하면 실행되는 메소드
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// WebSocketSession으로부터 받은 JSON 형태의 TextMessage를 ChatMessage 객체로 변환
// objectMapper.readValue() : JSON 데이터를 java 객체로 변환
// message.getPayload() : WebSocketSession 에서 받은 메시지 내용 반환
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
String cmd = chatMessage.getCmd();
if ("chat-open".equals(cmd)) {
openChatRoom(session, chatMessage);
} else if ("chat-close".equals(cmd)) {
closeChatRoom(session, chatMessage);
} else if ("chat-message".equals(cmd)) {
chatting(session, chatMessage);
}
}
// 클라이언트와 웹 소켓 연결이 종료되면 실행되는 메소드 - 해당 세션 제거
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
removeWebSocketSession(session);
}
// 클라이언트와 웹 소켓을 통해서 메시지 교환 중 오류가 발생하면 실행되는 메소드 - 해당 세션 제거
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
removeWebSocketSession(session);
}
// cmd 상태가 "chat-open"일 때 실행되는 메소드 - 채팅방 생성
private void openChatRoom(WebSocketSession session, ChatMessage chatMessage) throws Exception {
String customerId = chatMessage.getCustomerId();
// 대기중인 상담원이 없을 경우 메시지 전달
if (waitingEmployeeSessions.isEmpty()) {
ChatMessage responseMessage = new ChatMessage();
responseMessage.setCmd("chat-error");
responseMessage.setText("대기중인 상담원이 없습니다.");
// session.sendMessage : WebSocketSession 객체를 통해 연결된 클라이언트에게 메시지 보내는 기능
// objectMapper.writeValueAsBytes() : java 객체를 JSON 문자열로 변환
// 클라이언트에게 전송할 응답 메시지를 담고있는 ChatMessage 객체를 JSON 문자열로 변환하고,
// TextMessage 객체를 생성해서 해당 JSON 문자열을 클라이언트에게 전달
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
} else {
/*
* 대기중인 상담원이 있는 경우 새로운 채팅방 roonId 생성. 이를 위해 UUID를 생성하고 첫번째 대기중인 상담원의 ID를 가져옴
* UUID : 일련의 문자열을 생성 UUID.randomUUID() : 랜덤한 UUID 객체 생성 toString() : 문자열로 변환
*/
String uuid = UUID.randomUUID().toString();
/*
* waitingEmployeeSessions 맵에서 첫번째 직원의 WebSocketSession을 가져와 employeeId 변수에 할당
* keySet() : 맵의 key들을 Set 형태로 반환
* stream() : key들의 스트림 생성
* findFirst() : 스트림에서 첫번째 값만 가져옴 (waitingEmployeeSession 맵에서 첫번째 key)
* get() : 해당 key에 대응하는 value인 WebSocketSession 객체 가져옴
*/
String employeeId = waitingEmployeeSessions.keySet().stream().findFirst().get();
WebSocketSession employeeSession = waitingEmployeeSessions.get(employeeId);
// 상담원세션, 고객세션으로 채팅방 생성
Map<String, WebSocketSession> chatRoom = new HashMap<>();
chatRoom.put(customerId, session);
chatRoom.put(employeeId, employeeSession);
chatRooms.put(uuid, chatRoom);
// 대기중인 상담원 목록에서 상담원 제거
waitingEmployeeSessions.remove(customerId);
// 생성된 채팅방에 대해 클라이언트에게 채팅방이 생성되었음을 알리는 메시지 전송
ChatMessage responseMessage = new ChatMessage();
responseMessage.setCmd("chat-open-success");
responseMessage.setRoomId(uuid);
responseMessage.setCustomerId(customerId);
responseMessage.setEmployeeId(employeeId);
responseMessage.setText("대기중인 상담직원과 연결되었습니다.");
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
// 생성된 채팅방에 대해 대기중인 상담원에게도 채팅방이 생성되었음을 알리는 메시지 전송
responseMessage.setText("대기중인 고객과 연결되었습니다.");
employeeSession.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
}
// cmd 상태가 "chat-close"일 때 세션을 종료하고 채팅방 닫음
private void closeChatRoom(WebSocketSession session, ChatMessage chatMessage) throws Exception {
String roomId = chatMessage.getRoomId();
String employeeId = chatMessage.getEmployeeId();
// 해당 채팅방에 참여한 WebSocketSession 객체들을 얻어옴
Map<String, WebSocketSession> chatRoom = chatRooms.get(roomId);
// 해당 채팅방에서 상담했던 상담원의 WebSocketSession 객체를 waitingEmployeeSession에 다시 추가
WebSocketSession employeeSession = chatRoom.get(employeeId);
waitingEmployeeSessions.put(employeeId, employeeSession);
chatRooms.remove(roomId);
ChatMessage responseMessage = new ChatMessage();
responseMessage.setCmd("chat-close-success");
responseMessage.setText("상담이 종료되었습니다.");
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
// 생성된 채팅방에 대해 대기중인 상담원에게도 채팅방이 생성되었음을 알리는 메시지 전송
responseMessage.setText("상담이 종료되었습니다.");
employeeSession.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
session.close();
}
// cmd 상태가 "chat-message"일 때
private void chatting(WebSocketSession session, ChatMessage chatMessage) throws Exception {
String roomId = chatMessage.getRoomId();
String customerId = chatMessage.getCustomerId();
String employeeId = chatMessage.getEmployeeId();
String senderType = chatMessage.getSenderType();
Map<String, WebSocketSession> chatRoom = chatRooms.get(roomId);
if ("사용자".equals(senderType)) {
chatRoom.get(employeeId).sendMessage(new TextMessage(objectMapper.writeValueAsBytes(chatMessage)));
} else if ("관리자".equals(senderType)) {
chatRoom.get(customerId).sendMessage(new TextMessage(objectMapper.writeValueAsBytes(chatMessage)));
}
}
// 세션 제거
private void removeWebSocketSession(WebSocketSession session) throws Exception {
String loginId = (String) session.getAttributes().get("LOGIN_ID");
String loginType = (String) session.getAttributes().get("LOGIN_TYPE");
if ("사용자".equals(loginType)) {
customerSessions.remove(loginId);
} else if ("관리자".equals(loginType)) {
waitingEmployeeSessions.remove(loginId);
}
destroyChatRoom(loginId);
}
// 로그인 아이디와 관련된 모든 채팅방 제거
private void destroyChatRoom(String loginId) {
// 모든 채팅방을 가져옴 (chatRooms 맵의 키(채팅방 번호)를 저장하는 Set 객체)
Set<String> roomIdSet = chatRooms.keySet();
// Iterator : 컬렉션에 저장된 요소들에 대해 반복작업 수행
// .iterator() : Iterator 인터페이스를 구현핸 객체 반환. 이 객체로 Set 컬렉션의 요소들에 순서대로 접근
Iterator<String> iterator = roomIdSet.iterator();
// 모든 채팅방 순회
while (iterator.hasNext()) {
// 현재 처리중인 방 번호를 얻음
String roomId = iterator.next();
// 해당 방번호를 키로 갖는 map 객체에서 채팅을 종료한 사용자의 아이디를 갖고있는 WebSocketSession 객체를 찾음
Map<String, WebSocketSession> chatRoom = chatRooms.get(roomId);
// 채팅을 종료한 사용자의 아이디를 갖고 있는 WebSocketSession 객체가 존재하면 채팅방에서 그 객체 삭제
if (chatRoom.containsKey(loginId)) {
chatRoom.remove(loginId);
chatRooms.remove(roomId);
}
}
}
}
* ChatSocketHandler
chatRooms : 상담 중인 방
customerSessions : 고객의 WebSocketSession 리스트
waitingEmployeeSessions : 직원의 WebSocketSession 리스트 를 가지고 있음
cmd : 채팅 명령 -> cmd를 분석해서 보냄
* 자동으로 생성되는 WebSocketSession
- 텍스트만 달라지는 내용을 웹 소켓 세션에 저장해서 전달
- 웹 소켓 연결을 통해 서버와 클라이언트 간에 양방향 통신을 가능하게 하는 네트워크 프로토콜
- 웹 소켓 세션은 세션 ID, 연결 상태 및 메시지 전송을 위한 프로토콜과 같은 구성 요소를 포함
- 웹 소켓 세션의 구성
- 세션 ID: 웹 소켓 연결을 식별하기 위한 고유한 세션 ID. 이 세션 ID는 서버와 클라이언트 모두에게 부여
- 연결 상태: 웹 소켓 연결의 현재 상태를 나타내는 변수. 연결이 열려 있으면 "Open" 상태이고, 닫혀 있으면 "Closed" 상태
- 프로토콜: 웹 소켓 연결에 사용되는 프로토콜은 HTTP와는 다름. 웹 소켓은 다양한 프로토콜을 사용할 수 있으며, 대표적인 예로는 WebSocket, WAMP, Socket.IO 등이 있음.
- 메시지: 웹 소켓 세션을 통해 전송되는 메시지는 두 가지 유형 중 첫 번째 유형은 텍스트 메시지, 두 번째 유형은 이진 메시지. 둘 다 브라우저에서 JavaScript API를 통해 보내거나 서버에서 보내거나 받을 수 있음
- 이벤트: 웹 소켓 연결에는 예를 들어 연결이 열릴 때 "open" 이벤트, 메시지가 도착할 때 "message" 이벤트, 연결이 닫힐 때 "close" 이벤트 등이 있음. 이러한 이벤트를 처리하는 데 사용되는 JavaScript API가 있음.
- 하트비트: 일정한 시간 간격으로 클라이언트와 서버 간에 "하트비트" 메시지를 보내서 연결이 여전히 활성화되어 있는지 확인할 수 있음.
5. userChat.jsp
채팅 화면
<div class="container">
<div class="row" id="box-chat">
<div class="row">
<div class="col-12 text-center">
<h1><strong>상담 페이지</strong></h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card" id="card-chat">
<div class="card-header">상담내용</div>
<div class="card-body" style="height: 500px; overflow-y:scroll;"></div>
<div class='card-footer'>
<div class="row">
<div class="col-9">
<input type="text" class="form-control" name="message"/>
</div>
<div class="col">
<button class="btn btn-dark">전송</button>
<button id="btn-exit" class="btn btn-secondary">종료</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
자바스크립트 라이브러리 추가
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
자바스크립트
ws = new SockJS("/chat");
: 고객, 직원이 /chat이라는 요청을 보냄 -> 웹 소켓 연결 요청
-> 전용으로 통신하는 WebSocketSession이 생성됨 -> 이 소켓은 고객, 고객용, 직원용, 직원이 각자 가지고 있고 각자 연결되어 있어서 연결을 끊기 전까지는 계속 유지
-> 웹서버에서는 이 소켓세션이 각 고객, 직원별로 다 있어야 하니까 List에 담아둠
( 고객, 직원이 구분되어 있지 않다면 sessions는 하나만 있으면 됨 )
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
$(function() {
// 웹소켓 객체를 저장하는 변수
let ws = null;
// 채팅방 번호를 저장하는 변수
let roomId = null;
// 현재 상담중인 직원아이디가 저장되는 변수
let employeeId = null;
// 로그인한 사용자아이디가 저장되는 변수
let customerId = '<sec:authentication property="principal.id" />';
// 웹소켓 연결요청을 보내는 메소드
function connect() {
// 웹소켓 객체를 생성하고, 지정된 URI로 웹소켓 연결 요청
ws = new SockJS("/chat");
// 웹소켓 연결이 완료되면 실행되는 함수
ws.onopen = function() {
openChat();
}
// 웹소켓으로 서버로부터 메시지를 수신하면 실행된다.
ws.onmessage = function(message) {
console.log(message);
// 웹소켓으로 받은 데이터를 JSON 형식으로 파싱 (JSON 문자열을 자바스크립트 객체로 변환)
const data = JSON.parse(message.data);
console.log(data);
if (data.cmd == "chat-open-success") {
roomId = data.roomId;
employeeId = data.employeeId;
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
} else if (data.cmd == 'chat-message') {
appendChatMessage(data.text, 'float-start', 'alert-success', 'text-start');
} else if (data.cmd == 'chat-error'){
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
} else if (data.cmd == 'chat-close-success') {
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
disconnect();
}
}
}
// 웹소켓 연결 요청
connect();
// 웹소켓 연결 해제
function disconnect() {
ws.close();
}
// 웹소켓으로 상담시작 요청
function openChat() {
const message = {
cmd: 'chat-open',
customerId: customerId,
senderType: "사용자"
}
send(message);
}
// 웹소켓으로 상담중단 요청
function closeChat(){
const message = {
cmd: 'chat-close',
roomId: roomId,
customerId: customerId,
employeeId: employeeId,
senderType: "사용자"
}
send(message);
}
// 웹소켓으로 상담메시지 전달
function chat() {
const inputMessage = $(":input[name='message']").val();
if (inputMessage) {
const message = {
cmd: 'chat-message',
roomId: roomId,
customerId: customerId,
employeeId: employeeId,
senderType: "사용자",
text: inputMessage
}
send(message);
appendChatMessage(inputMessage, 'float-end', 'alert-warning', 'text-end');
$(":input[name='message']").val("");
}
}
function send(message) {
ws.send(JSON.stringify(message));
}
function appendChatMessage(message, floating, style, align){
const content = `
<div class="w-75 \${floating}">
<div class="alert \${style} \${align}">
\${message}
</div>
</div>
`;
$("#card-chat .card-body").append(content);
}
$("#card-chat .card-footer button").click(function(){
chat();
});
$(":input[name='message']").keydown(function(event){
if (event.which == 13) {
chat();
}
})
// 종료 버튼 클릭 시 채팅 종료
$("#btn-exit").click(function() {
closeChat();
})
})
</script>
</body>
</html>
6. empChat.jsp 도 동일
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="/resources/css/common.css">
<link rel="stylesheet" href="/resources/css/background.css">
<link rel="stylesheet" href="/resources/css/chat.css">
<title>중앙피트니스</title>
</head>
<body class="pt-5">
<%@ include file="../common/header.jsp" %>
<div class="container">
<div class="row" id="box-chat">
<div class="row">
<div class="col-12 text-center">
<h1><strong>고객 응대 페이지</strong></h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card" id="card-chat">
<div class="card-header">상담내용</div>
<div class="card-body" style="height: 500px; overflow-y:scroll;"></div>
<div class='card-footer'>
<div class="row">
<div class="col-10">
<input type="text" class="form-control" name="message"/>
</div>
<div class="col-2"><button class="btn btn-secondary">전송</button></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
$(function() {
// 웹소켓 객체를 저장하는 변수
let ws = null;
// 채팅방 번호를 저장하는 변수
let roomId = null;
// 현재 상담중인 직원아이디가 저장되는 변수
let employeeId = '<sec:authentication property="principal.id" />';
// 로그인한 사용자아이디가 저장되는 변수
let customerId = null;
// 웹소켓 연결요청을 보내는 메소드
function connect() {
// 웹소켓 객체를 생성하고, 지정된 URI로 웹소켓 연결 요청
ws = new SockJS("/chat");
// 웹소켓으로 서버로부터 메시지를 수신하면 실행된다.
ws.onmessage = function(message) {
// 웹소켓으로 받은 데이터를 JSON 형식으로 파싱 (JSON 문자열을 자바스크립트 객체로 변환)
const data = JSON.parse(message.data);
if (data.cmd == "chat-open-success") {
roomId = data.roomId;
customerId = data.customerId;
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
} else if (data.cmd == 'chat-message') {
appendChatMessage(data.text, 'float-start', 'alert-success', 'text-start');
} else if (data.cmd == 'chat-error'){
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
} else if (data.cmd == 'chat-close-success') {
appendChatMessage(data.text, 'float-start', 'alert-danger', 'text-start');
disconnect();
}
}
}
// 웹소켓 연결 요청
connect();
// 웹소켓 연결 해제
function disconnect() {
ws.close();
}
// 웹소켓으로 상담메시지 전달
function chat() {
const inputMessage = $(":input[name='message']").val();
if (inputMessage) {
const message = {
cmd: 'chat-message',
roomId: roomId,
customerId: customerId,
employeeId: employeeId,
senderType: "관리자",
text: inputMessage
}
send(message);
appendChatMessage(inputMessage, 'float-end', 'alert-warning', 'text-end');
$(":input[name='message']").val("");
}
}
function send(message) {
ws.send(JSON.stringify(message));
}
function appendChatMessage(message, floating, style, align){
const content = `
<div class="w-75 \${floating}">
<div class="alert \${style} \${align}">
\${message}
</div>
</div>
`;
$("#card-chat .card-body").append(content);
}
$("#card-chat .card-footer button").click(function(){
chat();
});
$(":input[name='message']").keydown(function(event){
if (event.which == 13) {
chat();
}
})
})
</script>
</body>
</html>
'수업내용 > 프로젝트' 카테고리의 다른 글
[Spring/Security] 소셜 로그인 (0) | 2023.03.02 |
---|---|
[Spring] fullcalendar 적용하기 (async/await, lodash 사용) (0) | 2023.02.21 |
[Spring/mybatis] resultMap (한 아이디에 해당하는 항목리스트 불러오기) (0) | 2023.02.20 |
[Spring/mybatis] 검색, 정렬기능 (0) | 2023.02.20 |
[Spring] String joiner 요일 리스트를 테이블 한 칸에 표시하기 (0) | 2023.02.17 |