코딩 노트

웹소켓 - 실시간 채팅, 반응형 디자인 본문

Spring

웹소켓 - 실시간 채팅, 반응형 디자인

newbyeol 2023. 10. 23. 11:27

http가 없으면 websocket가 있을 수 없다.

http 주소와 websocket 둘이 기반이 다르면(주소가 다르면) 안 된다.

지금의 문제점

1. 주소가 꼭 ws로 시작해야되나? 어차피 http 기반이면 주소도 안 겹치게 만들어놨는데

그냥 대충 http로 시작하게 알아서 접속해줬으면...

 

2. 구버전 브라우저에서 웹소켓이 안 되는데...

그러면 아쉬운대로 Pulling 방식으로라도 흉내낸다면 사용자는 좀 더 좋아하지 않을까?

 

3. 갑자기 종료되는 사용자 중에 체크가 안 되는 것들이 있다. (Dead connection)

주기적으로 사용자가 접속해있는지 체크하도록 처리하고 싶다. (라이브 핑을 보낸다/대화 목적이 아님 확인 목적)

-- 이 세가지를 해결해주는 기술이 SockJS이다.

 

윈도우에 만드는 이유는 아무데서나 쓰기 위해서이다.

SockJsWebSocketServer 파일 생성

@Slf4j @Service

public class SockJsWebSocketServer extends TextWebSocketHandler{

//저장소 생성

 

//동기화 처리가 되어있어 조금 느리지만 사용자가 동시다발적으로 나가거나 들어오는 상황에서 유리.

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

 

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

clients.add(session);

log.debug("사용자 접속! 현재 {}명", clients.size());

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

clients.remove(session);

log.debug("사용자 접속! 현재 {}명", clients.size());

}

 

}

WebSocketServerConfiguration 파일 코드 추가

@Autowired

private SockJsWebSocketServer sockJsWebSocketServer;

//SockJS를 사용하는 웹소켓 서버는 뒤에 추가적인 설정을 해야 한다.

//- 클라이언트도 이 웹소켓 서버에 연결하려면 SockJS를 사용해야 한다. (위 코드에 붙이면 원래 예제가 하나도 안 돌아감)

registry.addHandler(sockJsWebSocketServer, "/ws/sockjs")

.addInterceptors(new HttpSessionHandshakeInterceptor())

.withSockJS();

 

}

WebSocketController에 구문 추가

@RequestMapping("/sockjs")

public String sockjs() {

return "sockjs";

}

sockjs.jsp 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>SockJS를 적용한 웹소켓 예제</h1>

 

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다. -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

 

<script>

//연결 생성

//window.socket = new WebSocket(주소); //ws로 시작하는 주소

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs"); //http로 시작하는 주소

 

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){ //서버에서 오는 메세지

console.log(e);

};

 

</script>

home.jsp에 바로가기 구문 추가

<h2><a href="sockjs">SockJS를 적용한 웹소켓 예제</a></h2>

aaa / bbb / ccc / ddd / eee 회원이 있다.

session(attributes=[name=aaa]) 저장이 이렇게 되어있다.

aaa가 eee에게 메세지를 보낼 수 있는가?

- 모든 저장소의 session을 조회해서

- 그 안에 있는 attributes를 꺼낸 뒤

- 아이디 유무를 파악해서 일치하는지 조사

- 일치한다면 해당 세션에 메세지를 전송

 

vo 패키지 생성 후 ClientVO 파일 생성

//웹소켓 통신에서 사용자를 조금 더 편하게 관리하기 위한 클래스

@Data

public class ClientVO {

private WebSocketSession session;

private String memberId, memberLevel; //비회원이라면 null일것

 

public ClientVO(WebSocketSession session) { //무조건 session이 필요하기 때문에 생성자로 만듬

this.session = session;

Map<String, Object> attr = session.getAttributes();

this.memberId = (String) attr.get("name"); //없으면 null이 들어가기 때문에

this.memberLevel = (String) attr.get("level"); //문제가 안 됨

}

 

public boolean isMember() {

return memberId != null && memberLevel != null; //사용자가 회원인지 아닌지 알 수 있음

}

 

public void send(TextMessage message) throws IOException {

session.sendMessage(message);

}

}

자바에서 비교 기준을 변경하던 것

SockJsWebSocketServer 수정

@Slf4j @Service

public class SockJsWebSocketServer extends TextWebSocketHandler{

//저장소 생성

 

//동기화 처리가 되어있어 조금 느리지만 사용자가 동시다발적으로 나가거나 들어오는 상황에서 유리.

// private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

private Set<ClientVO> clients = new CopyOnWriteArraySet<>();

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

ClientVO client = new ClientVO(session);

clients.add(client);

log.debug("접속한 사용자 = {}", client);

log.debug("사용자 접속! 현재 {}명", clients.size());

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

ClientVO client = new ClientVO(session);

clients.remove(client);

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//클라이언트가 여러 개가 있을 때 어떻게 처리하나?(아이디가 같다고 해서 같은 애가 아님)

 

}

}

ClientVO 수정

//웹소켓 통신에서 사용자를 조금 더 편하게 관리하기 위한 클래스

@Data

@EqualsAndHashCode(of = "session") //session 필드가 동일하면 같은 객체라고 생각해라

@ToString(of = {"memberId", "memberLevel"}) //요약 정보에서 세션을 제거하게 함 // 출력할 때 작성한 항목만 출력해라!

public class ClientVO {

private WebSocketSession session;

private String memberId, memberLevel; //비회원이라면 null일것

 

public ClientVO(WebSocketSession session) { //무조건 session이 필요하기 때문에 생성자로 만듬

this.session = session;

Map<String, Object> attr = session.getAttributes();

this.memberId = (String) attr.get("name"); //없으면 null이 들어가기 때문에

this.memberLevel = (String) attr.get("level"); //문제가 안 됨

}

 

public boolean isMember() {

return memberId != null && memberLevel != null; //사용자가 회원인지 아닌지 알 수 있음

}

 

public void send(TextMessage message) throws IOException {

session.sendMessage(message);

}

}

모든 사용자를 JSON으로 만들어 클라이언트에게 접속하거나 접속이 종료됐을 때 정보를 보냄

SockJsWebSocketServer

@Slf4j @Service

public class SockJsWebSocketServer extends TextWebSocketHandler{

//저장소 생성

 

//동기화 처리가 되어있어 조금 느리지만 사용자가 동시다발적으로 나가거나 들어오는 상황에서 유리.

// private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

private Set<ClientVO> clients = new CopyOnWriteArraySet<>(); //순서를 기억해주고 싶다면 리스트로 바꿔야 함

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

ClientVO client = new ClientVO(session);

clients.add(client);

log.debug("접속한 사용자 = {}", client);

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

ClientVO client = new ClientVO(session);

clients.remove(client);

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//클라이언트가 여러 개가 있을 때 어떻게 처리하나?(아이디가 같다고 해서 같은 애가 아님)

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

 

//접속자 명단(clients)을 모든 접속자에게 전송하는 메소드

public void sendClientList() throws IOException {

//1. clients를 전송 가능한 형태(JSON 문자열)로 변환한다.

ObjectMapper mapper = new ObjectMapper();

 

Map<String, Object> data = new HashMap<>();

data.put("clients", clients);

String clientJson = mapper.writeValueAsString(data);

 

//2. 모든 사용자에게 전송

TextMessage message = new TextMessage(clientJson);

for (ClientVO client : clients) {

client.send(message);

}

}

}

ClientVO 수정

//웹소켓 통신에서 사용자를 조금 더 편하게 관리하기 위한 클래스

@Data

@EqualsAndHashCode(of = "session") //session 필드가 동일하면 같은 객체라고 생각해라

@ToString(of = {"memberId", "memberLevel"}) //요약 정보에서 세션을 제거하게 함 // 출력할 때 작성한 항목만 출력해라!

public class ClientVO {

@JsonIgnore //Json으로 변환하는 과정에서 (입출력에서) 이 필드는 제외한다.

private WebSocketSession session;

private String memberId, memberLevel; //비회원이라면 null일것

 

public ClientVO(WebSocketSession session) { //무조건 session이 필요하기 때문에 생성자로 만듬

this.session = session;

Map<String, Object> attr = session.getAttributes();

this.memberId = (String) attr.get("name"); //없으면 null이 들어가기 때문에

this.memberLevel = (String) attr.get("level"); //문제가 안 됨

}

 

public boolean isMember() {

return memberId != null && memberLevel != null; //사용자가 회원인지 아닌지 알 수 있음

}

 

public void send(TextMessage message) throws IOException {

session.sendMessage(message);

}

}

transient (입출력에서 이 필드는 제외한다. 쓰긴 쓰지만 저장은 안 하겠다.) (여기선 쓰지 않고 JsonIgnore로)

SockJsWebSocketServer

@Slf4j @Service

public class SockJsWebSocketServer extends TextWebSocketHandler{

//저장소 생성

 

//동기화 처리가 되어있어 조금 느리지만 사용자가 동시다발적으로 나가거나 들어오는 상황에서 유리.

// private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

private Set<ClientVO> clients = new CopyOnWriteArraySet<>(); //순서를 기억해주고 싶다면 리스트로 바꿔야 함 //전체회원

private Set<ClientVO> members = new CopyOnWriteArraySet<>(); //로그인한 회원

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

ClientVO client = new ClientVO(session);

clients.add(client);

 

if(client.isMember()) { //회원이라면

members.add(client);

}

log.debug("접속한 사용자 = {}", client);

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

ClientVO client = new ClientVO(session);

clients.remove(client);

 

if(client.isMember()) { //회원이라면

members.remove(client);

}

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//클라이언트가 여러 개가 있을 때 어떻게 처리하나?(아이디가 같다고 해서 같은 애가 아님)

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

 

//접속자 명단(clients)을 모든 접속자에게 전송하는 메소드

public void sendClientList() throws IOException {

//1. clients를 전송 가능한 형태(JSON 문자열)로 변환한다.

ObjectMapper mapper = new ObjectMapper();

 

Map<String, Object> data = new HashMap<>();

// data.put("clients", clients); //전체회원명단 (null)이 문제가 됨

data.put("clients", members); //로그인한 회원 명단

String clientJson = mapper.writeValueAsString(data);

 

//2. 모든 사용자에게 전송

TextMessage message = new TextMessage(clientJson);

for (ClientVO client : clients) {

client.send(message);

}

}

}

sockjs.jsp 수정 

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>SockJS를 적용한 웹소켓 예제</h1>

<div class="client-list"></div>

 

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다. -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

 

<script>

//연결 생성

//window.socket = new WebSocket(주소); //ws로 시작하는 주소

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs"); //http로 시작하는 주소

 

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){ //서버에서 오는 메세지

//console.log(e);

var data = JSON.parse(e.data);

//console.log(data);

 

//data.clients에 회원 목록이 있다.

$(".client-list").empty();

for(var i=0; i < data.clients.length; i++){

$("<div>").text(data.clients[i].memberId).appendTo(".client-list");

}

};

 

</script>

서버에서 전달되는 데이터의 형태

e.data에는 두 가지의 데이터가 있다.

 

조건(content라는 항목이 데이터에 포함되어 있다면 메세지로 간주)

사용자의 메세지를 브로드캐스트 하는 경우

{"memberId":???, "memberLevel":???, "content":???}

 

조건(clients라는 항목이 데이터에 포함되어 있다면 목록으로 간주)

사용자가 접속하거나 종료하여 목록을 보내는 경우

{"clients":[...]}

SockJsWebSocketServer

@Slf4j @Service

public class SockJsWebSocketServer extends TextWebSocketHandler{

//저장소 생성

 

//동기화 처리가 되어있어 조금 느리지만 사용자가 동시다발적으로 나가거나 들어오는 상황에서 유리.

// private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

private Set<ClientVO> clients = new CopyOnWriteArraySet<>(); //순서를 기억해주고 싶다면 리스트로 바꿔야 함 //전체회원

private Set<ClientVO> members = new CopyOnWriteArraySet<>(); //로그인한 회원

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

ClientVO client = new ClientVO(session);

clients.add(client);

 

if(client.isMember()) { //회원이라면

members.add(client);

}

log.debug("접속한 사용자 = {}", client);

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

ClientVO client = new ClientVO(session);

clients.remove(client);

 

if(client.isMember()) { //회원이라면

members.remove(client);

}

log.debug("사용자 접속! 현재 {}명", clients.size());

 

//클라이언트가 여러 개가 있을 때 어떻게 처리하나?(아이디가 같다고 해서 같은 애가 아님)

 

//모든 접속자에게 접속자 명단을 전송

sendClientList();

}

 

//접속자 명단(clients)을 모든 접속자에게 전송하는 메소드

public void sendClientList() throws IOException {

//1. clients를 전송 가능한 형태(JSON 문자열)로 변환한다.

ObjectMapper mapper = new ObjectMapper();

 

Map<String, Object> data = new HashMap<>();

// data.put("clients", clients); //전체회원명단 (null)이 문제가 됨

data.put("clients", members); //로그인한 회원 명단

String clientJson = mapper.writeValueAsString(data);

 

//2. 모든 사용자에게 전송

TextMessage message = new TextMessage(clientJson);

for (ClientVO client : clients) {

client.send(message);

}

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

// 사용자가 보낸 메세지를 모두에게 broadcast

ClientVO client = new ClientVO(session);

if(client.isMember() == false) return;

 

//정보를 Map에 담아서 변환 후 전송

Map<String, Object> map = new HashMap<>();

map.put("memberId", client.getMemberId());

map.put("memberLevel", client.getMemberLevel());

map.put("content", message.getPayload());

 

//시간 추가 등 가능

 

ObjectMapper mapper = new ObjectMapper();

String messageJson = mapper.writeValueAsString(map); //json 변환

TextMessage tm = new TextMessage(messageJson); //전송 가능한 텍스트 형태의 메세지로

 

for(ClientVO c : clients) {

c.send(tm); //메세지를 보냄

}

}

}

sockjs.jsp 수정

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>SockJS를 적용한 웹소켓 예제</h1>

 

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<div class="client-list"></div>

<div class="message-list"></div>

 

 

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다. -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

 

<script>

//연결 생성

//window.socket = new WebSocket(주소); //ws로 시작하는 주소

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs"); //http로 시작하는 주소

 

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){ //서버에서 오는 메세지

//console.log(e);

var data = JSON.parse(e.data);

//console.log(data);

 

//사용자가 접속하거나 종료했을 때 서버에서 오는 데이터로 목록을 갱신

//사용자가 메세지를 보냈을 때 서버에서 이를 전체에게 전달한다.

//data.clients에 회원 목록이 있다.

if(data.clients ) { //목록 처리

$(".client-list").empty();

for(var i=0; i < data.clients.length; i++){

$("<div>").text(data.clients[i].memberId).appendTo(".client-list");

}

}

else if(data.content) { //메세지 처리

var memberId = $("<div>").text(data.memberId);

var memberLevel = $("<div>").text(data.memberLevel);

var content = $("<div>").text(data.content);

 

$("<div>").css("display", "flex")

.append(memberId)

.append(memberLevel)

.append(content)

.appendTo(".message-list");

}

};

 

$(".send-btn").click(function(){

var text = $(".message-input").val();

if(text.length == 0) return;

 

window.socket.send(text);

$(".message-input").val("")

});

 

</script>

sockjs.jsp 수정 - 디자인 1차 구현

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<!doctype html>

<html lang="ko">

 

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Bootstrap demo</title>

 

<!-- 아이콘 사용을 위한 Font Awesome 6 CDN -->

<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">

 

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">

<link href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/sandstone/bootstrap.min.css" rel="stylesheet">

<link href="test.css" rel="stylesheet">

</head>

 

<body>

<div class="container-fluid">

<div class="row">

<div class="col-md-10 offset-md-1">

 

<div class="row mt-4">

<div class="col">

<h1>전체 채팅</h1>

</div>

</div>

 

<div class="row mt-4">

<div class="col-4 client-list"></div>

<div class="col-8">

 

<div class="row">

<div class="col">

<div class="input-group">

<input type="text" class="form-control message-input" placeholder="메세지 내용 작성">

<button type="button" class="btn btn-primary send-btn">

<i class="fa-regular fa-paper-plane"></i>

보내기

</button>

</div>

</div>

</div>

 

<!-- 메세지 표시 영역 -->

<div class="row mt-4">

<div class="col message-list"></div>

</div>

 

</div>

</div>

 

</div>

</div>

</div>

 

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다 -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

<script>

//연결 생성

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs");

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){

//console.log(e);

var data = JSON.parse(e.data);

//console.log(data);

 

//사용자가 접속하거나 종료했을 때 서버에서 오는 데이터로 목록을 갱신

//사용자가 메세지를 보냈을 때 서버에서 이를 전체에게 전달한다

//data.clients에 회원 목록이 있다

if(data.clients) {//목록 처리

$(".client-list").empty();

 

var ul = $("<ul>").addClass("list-group");

for(var i=0; i < data.clients.length; i++) {

$("<li>")

.addClass("list-group-item d-flex justify-content-between align-items-center")

.text(data.clients[i].memberId)

.append(

$("<span>").addClass("badge bg-primary badge-pill")

.text(data.clients[i].memberLevel)

)

.appendTo(ul);

}

ul.appendTo(".client-list");

}

else if(data.content) {//메세지 처리

var memberId = $("<strong>").text(data.memberId);

var memberLevel = $("<span>").text(data.memberLevel)

.addClass("badge bg-primary badge-pill ms-2");

var content = $("<div>").text(data.content);

 

$("<div>").addClass("border border-secondary rounded p-2 mt-2")

.append(memberId)

.append(memberLevel)

.append("<hr>")

.append(content)

.appendTo(".message-list");

}

};

 

$(".send-btn").click(function(){

var text = $(".message-input").val();

if(text.length == 0) return;

 

window.socket.send(text);

$(".message-input").val("");

});

</script>

</body>

 

</html>

sockjs.jsp 수정 - 줄어드는 화면에 대한 반응형 처리

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<!doctype html>

<html lang="ko">

 

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Bootstrap demo</title>

 

<!-- 아이콘 사용을 위한 Font Awesome 6 CDN -->

<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">

 

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">

<link href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/sandstone/bootstrap.min.css" rel="stylesheet">

<link href="test.css" rel="stylesheet">

 

<style>

.btn-userlist {

display: none;

}

 

@media screen and (max-width:768px) {

.client-list {

position: fixed;

top:0;

left:-250px;

bottom:0;

width:250px;

z-index: 9999999;

padding-top: 90px;

transition:left 0.2s ease-out;

}

.client-list.active {

left:0;

}

.btn-userlist {

display:block;

position: fixed;

top:1em;

right:1em;

}

}

</style>

</head>

 

<body>

<div class="container-fluid">

<div class="row">

<div class="col-md-10 offset-md-1">

 

<div class="row mt-4">

<div class="col">

<h1>

전체 채팅

<button class="btn btn-secondary btn-userlist">

<i class="fa-solid fa-users"></i>

</button>

</h1>

</div>

</div>

 

<div class="row mt-4">

<div class="col-md-4 client-list"></div>

<div class="col-md-8">

 

<div class="row">

<div class="col">

<div class="input-group">

<input type="text" class="form-control message-input" placeholder="메세지 내용 작성">

<button type="button" class="btn btn-primary send-btn">

<i class="fa-regular fa-paper-plane"></i>

보내기

</button>

</div>

</div>

</div>

 

<!-- 메세지 표시 영역 -->

<div class="row mt-4">

<div class="col message-list"></div>

</div>

 

</div>

</div>

 

</div>

</div>

</div>

 

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다 -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

<script>

//연결 생성

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs");

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){

//console.log(e);

var data = JSON.parse(e.data);

//console.log(data);

 

//사용자가 접속하거나 종료했을 때 서버에서 오는 데이터로 목록을 갱신

//사용자가 메세지를 보냈을 때 서버에서 이를 전체에게 전달한다

//data.clients에 회원 목록이 있다

if(data.clients) {//목록 처리

$(".client-list").empty();

 

var ul = $("<ul>").addClass("list-group");

for(var i=0; i < data.clients.length; i++) {

$("<li>")

.addClass("list-group-item d-flex justify-content-between align-items-center")

.text(data.clients[i].memberId)

.append(

$("<span>").addClass("badge bg-primary badge-pill")

.text(data.clients[i].memberLevel)

)

.appendTo(ul);

}

ul.appendTo(".client-list");

}

else if(data.content) {//메세지 처리

var memberId = $("<strong>").text(data.memberId);

var memberLevel = $("<span>").text(data.memberLevel)

.addClass("badge bg-primary badge-pill ms-2");

var content = $("<div>").text(data.content);

 

$("<div>").addClass("border border-secondary rounded p-2 mt-2")

.append(memberId)

.append(memberLevel)

.append("<hr>")

.append(content)

.appendTo(".message-list");

}

};

 

$(".send-btn").click(function(){

var text = $(".message-input").val();

if(text.length == 0) return;

 

window.socket.send(text);

$(".message-input").val("");

});

 

//.btn-userlist를 누르면 사용자 목록에 active를 붙였다 떼었다 하도록 처리

$(".btn-userlist").click(function(){

$(".client-list").toggleClass("active");

});

</script>

</body>

 

</html>

sockjs.jsp 수정 - 디자인 구현 완료

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<!doctype html>

<html lang="ko">

 

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Bootstrap demo</title>

 

<!-- 아이콘 사용을 위한 Font Awesome 6 CDN -->

<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">

 

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">

<link href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/sandstone/bootstrap.min.css" rel="stylesheet">

<link href="test.css" rel="stylesheet">

 

<style>

.btn-userlist {

display: none;

}

.message-list {

height: 65vh;

overflow-y: scroll;

padding-bottom: 15px;

}

::-webkit-scrollbar {

width: 1px; /* 스크롤바 너비 */

background-color: black

}

::-webkit-scrollbar-thumb {

background: var(--bs-secondary); /* 스크롤바 색상 */

}

 

@media screen and (max-width:768px) {

.client-list {

position: fixed;

top:0;

left:-250px;

bottom:0;

width:250px;

z-index: 9999999;

padding-top: 90px;

transition:left 0.2s ease-out;

}

.client-list.active {

left:0;

}

.btn-userlist {

display:block;

position: fixed;

top:1em;

right:1em;

}

}

</style>

</head>

 

<body>

<div class="container-fluid">

<div class="row">

<div class="col-md-10 offset-md-1">

 

<div class="row mt-4">

<div class="col">

<h1>

전체 채팅

<button class="btn btn-secondary btn-userlist">

<i class="fa-solid fa-users"></i>

</button>

</h1>

</div>

</div>

 

<div class="row mt-4">

<div class="col-md-4 client-list"></div>

<div class="col-md-8">

 

<div class="row">

<div class="col">

<div class="input-group">

<input type="text" class="form-control message-input" placeholder="메세지 내용 작성">

<button type="button" class="btn btn-primary send-btn">

<i class="fa-regular fa-paper-plane"></i>

보내기

</button>

</div>

</div>

</div>

 

<!-- 메세지 표시 영역 -->

<div class="row mt-4">

<div class="col message-list"></div>

</div>

 

</div>

</div>

 

</div>

</div>

</div>

 

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

<!-- 웹소켓 서버가 SockJS일 경우 페이지에서도 SockJS를 사용해야 한다 -->

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

<script>

//연결 생성

window.socket = new SockJS("${pageContext.request.contextPath}/ws/sockjs");

//연결 후 해야할 일들을 콜백함수로 지정(onopen, onclose, onerror, onmessage)

window.socket.onmessage = function(e){

//console.log(e);

var data = JSON.parse(e.data);

//console.log(data);

 

//사용자가 접속하거나 종료했을 때 서버에서 오는 데이터로 목록을 갱신

//사용자가 메세지를 보냈을 때 서버에서 이를 전체에게 전달한다

//data.clients에 회원 목록이 있다

if(data.clients) {//목록 처리

$(".client-list").empty();

 

var ul = $("<ul>").addClass("list-group");

for(var i=0; i < data.clients.length; i++) {

$("<li>")

.addClass("list-group-item d-flex justify-content-between align-items-center")

.text(data.clients[i].memberId)

.append(

$("<span>").addClass("badge bg-primary badge-pill")

.text(data.clients[i].memberLevel)

)

.appendTo(ul);

}

ul.appendTo(".client-list");

}

else if(data.content) {//메세지 처리

var memberId = $("<strong>").text(data.memberId);

var memberLevel = $("<span>").text(data.memberLevel)

.addClass("badge bg-primary badge-pill ms-2");

var content = $("<div>").text(data.content);

 

//메세지를 화면에 추가

$("<div>").addClass("border border-secondary rounded p-2 mt-2")

.append(memberId)

.append(memberLevel)

.append("<hr>")

.append(content)

.appendTo(".message-list");

 

//스크롤바를 맨 아래로 이동

$(".message-list").scrollTop($(".message-list")[0].scrollHeight);

}

};

 

$(".send-btn").click(function(){

var text = $(".message-input").val();

if(text.length == 0) return;

 

window.socket.send(text);

$(".message-input").val("");

});

 

//.btn-userlist를 누르면 사용자 목록에 active를 붙였다 떼었다 하도록 처리

$(".btn-userlist").click(function(){

$(".client-list").toggleClass("active");

});

</script>

</body>

 

</html>