수업내용/프로젝트

[Spring/Security] Web socket 을 이용한 채팅기능

주니어주니 2023. 3. 21. 21:06

 

 

 

 

 

  • 개발환경
    • 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>