코딩 노트
웹소켓 - 실시간 채팅, 반응형 디자인 본문
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>
'Spring' 카테고리의 다른 글
결제01 - (카카오페이 api) + 카멜케이스 변환 (0) | 2023.10.24 |
---|---|
DM을 어떻게 보내는가 (0) | 2023.10.23 |
실시간 채팅(WebSocket) (0) | 2023.10.20 |
HTTP 쿠키(Cookie) (0) | 2023.10.18 |
myBatis04 - 암호화 로그인 + view resolver + 회원가입 축하 이메일 (0) | 2023.10.17 |