안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
관리자 - 채팅 목록 불러오기
Controller
AdminController
@GetMapping("/chatList.do")
public void chatList(Model model) {
// 채팅방 별 최근 1건 조회
List<ChatLog> chatList = chatService.findRecentChatLogs();
log.debug("chatList = {}", chatList);
model.addAttribute("chatList", chatList);
}
Service (interface 생략)
ChatServiceImpl
@Override
public List<ChatLog> findRecentChatLogs() {
return chatDao.findRecentChatLogs();
}
Dao
ChatDao interface
@Select("select\r\n"
+ " no,\r\n"
+ " chatroom_id,\r\n"
+ " (select member_id from chat_member where chatroom_id = a.chatroom_id and member_id != 'admin')member_id, -- 채팅방에 관리자가 아닌 회원아이디 필요\r\n"
+ " msg,\r\n"
+ " time\r\n"
+ "from (\r\n"
+ " select\r\n"
+ " cl.*,\r\n"
+ " row_number() over(partition by chatroom_id order by no desc) rnum\r\n"
+ " from\r\n"
+ " chat_log cl\r\n"
+ ") a\r\n"
+ "where\r\n"
+ " rnum = 1")
List<ChatLog> findRecentChatLogs();
채팅방 별 가장 최신 메세지 하나만을 목록에서 보여줘야 하며,
만일 관리자의 메세지가 가장 최신이라면, 목록에는 'admin : 어쩌구' 가 보여지므로 어떤 회원에 대한 채팅방인지 모호해지기 때문에 이를 해결하고자 admin이 아닌 회원 아이디를 가져와 보여주었습니다.
group by가 아닌 row_number over를 이용하여 chatroom_id를 기준으로 rnum을 부여해주었고, where절을 통해 rnum이 1인 데이터만 가져와줍니다.
chatList.jsp
<jsp:include page="/WEB-INF/views/common/header.jsp">
<jsp:param value="채팅관리" name="title"/>
</jsp:include>
<table class="table text-center">
<thead>
<tr>
<th scope="col">회원아이디</th>
<th scope="col">메세지</th>
</tr>
</thead>
<tbody>
<c:forEach items="${chatList}" var="chat">
<tr>
<td>${chat.memberId}</td>
<td>${chat.msg}</td>
</tr>
</c:forEach>
</tbody>
</table>
<jsp:include page="/WEB-INF/views/common/footer.jsp"></jsp:include>
이처럼 가장 최신 메세지, admin이 아닌 채팅 회원 아이디를 보여주어 관리자 채팅 목록을 처리해주었습니다.
관리자 - 회원 별 채팅창 띄우기
- 채팅방 클릭 시 채팅창 팝업이 열리고 메세지를 전송
chatList.jsp
<script>
// 채팅방 별 채팅 팝업 생성
document.querySelectorAll("tr[data-chatroom-id]").forEach((tr) => {
tr.addEventListener('click', (e) => {
const chatroomId = e.target.parentElement.dataset.chatroomId;
console.log(chatroomId);
// 팝업
const url = `${pageContext.request.contextPath}/admin/chat.do?chatroomId=\${chatroomId}`;
const name = chatroomId; // window의 이름으로 사용
const spec = "width=500px, height=500px";
open(url, name, spec);
});
})
</script>
Controller
AdminController
@GetMapping("/chat.do")
public void chat(@RequestParam String chatroomId, Model model) {
List<ChatLog> chatLogs = chatService.findChatlogByChatroomId(chatroomId);
log.debug("chatLogs = {}", chatLogs);
model.addAttribute("chatLogs", chatLogs);
}
Service, Dao 생략
chat.jsp (팝업창)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${param.pageTitle}</title>
<script src="${pageContext.request.contextPath }/resources/js/jquery-3.6.0.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/css/style.css" />
<!-- WebSocket:sock.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.js"></script>
<!-- WebSocket: stomp.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
</head>
<body>
<div class="input-group mb-3">
<input type="text" id="message" class="form-control" placeholder="Message">
<div class="input-group-append" style="padding: 0px;">
<button id="sendBtn" class="btn btn-outline-secondary" type="button">Send</button>
</div>
</div>
<div>
<ul class="list-group list-group-flush" id="data">
<c:forEach items="${chatLogs}" var="chat">
<li class="list-group-item">${chat.memberId} : ${chat.msg}</li>
</c:forEach>
</ul>
</div>
</body>
</html>
이처럼 팝업창이 열리고 기존 채팅 내역들을 확인할 수 있습니다.
관리자 - 구독 처리 및 채팅 보내기
chat.jsp
<script>
const ws = new SockJS(`http://\${location.host}${pageContext.request.contextPath}/stomp`);
const stompClient = Stomp.over(ws);
stompClient.connect({}, (frame) => {
console.log('connect : ', frame);
stompClient.subscribe(`/app/chat/${param.chatroomId}`, (message) => {
console.log(`/app/chat/${param.chatroomId} : `, message);
const {'content-type':contextType} = message.headers;
if(!contextType) return;
const {memberId, msg, time} = JSON.parse(message.body);
const li = `<li class="list-group-item" title="\${time}">\${memberId} : \${msg}</li>`;
document.querySelector("#data").insertAdjacentHTML('beforeend', li);
});
});
// 관리자 채팅 보내기
document.querySelector("#sendBtn").addEventListener('click', (e) => {
const msg = document.querySelector("#message").value;
if(!msg) return;
const payload = {
chatroomId : `${param.chatroomId}`,
memberId : `<sec:authentication property="principal.username"/>`,
msg,
time : Date.now()
};
stompClient.send(`/app/chat/${param.chatroomId}`, {}, JSON.stringify(payload));
document.querySelector("#message").value = '';
});
</script>
관리자와의 채팅이 잘 되는 것을 확인할 수 있습니다.
관리자 - 채팅 시 최근 채팅 메세지와 최근 채팅이 상단에 조회되도록 처리
- chatList 페이지에서 별도 구독처리!
Controller
StompController
@MessageMapping("/chat/{chatroomId}")
@SendTo({"/app/chat/{chatroomId}", "/app/admin/chatList"})
public ChatLog chatLog(@RequestBody ChatLog chatlog) {
log.debug("chatlog = {}", chatlog);
int result = chatService.insertChatLog(chatlog);
return chatlog;
}
메세지를 보낼 때 '/app/admin/chatList'에도 보내주어 이를 chatList 페이지에서 구독 후 처리 해주겠습니다.
chatList.jsp
// html
<table class="table text-center" id="tbl-chat-list">
<thead>
<tr>
<th scope="col">회원아이디</th>
<th scope="col">메세지</th>
</tr>
</thead>
<tbody>
<c:forEach items="${chatList}" var="chat">
<tr data-chatroom-id="${chat.chatroomId}">
<td>${chat.memberId}</td>
<td class="msg">${chat.msg}</td>
</tr>
</c:forEach>
</tbody>
</table>
// script
// 채팅 목록 및 메세지 최신화 처리를 위한 구독
setTimeout(() => {
stompClient.subscribe('/app/admin/chatList', (message) => {
console.log(message);
const {chatroomId, memberId, msg} = JSON.parse(message.body);
const tr = document.querySelector(`tr[data-chatroom-id="\${chatroomId}"]`);
tr.querySelector(".msg").innerHTML = msg;
// 끌어올리기
// 새로 생성된 것이 아닌 기존 존재하는 태그이므로 잘라 붙여넣기 처리!
document.querySelector("#tbl-chat-list tbody").insertAdjacentElement('afterbegin', tr);
});
}, 500);
넘어온 메세지를 받아 구조분해할당 처리 후 해당 chatroomId를 dataset 속성으로 가지고 있는 tr태그를 찾아 msg를 업데이트 해줍니다.
또한, tbody를 제어하므로 insertAdjacentElement()를 이용하여 끌어올리기 처리 해주었습니다. 새로 생성된 태그가 아닌 기존 존재하는 태그이므로 insert처리가 아니라 잘라 붙여넣기 처리됩니다.
※ insertAdjacentHTML, insertAdjacentElement 차이
- insertAdjacentHTML(position, string)
- insertAdjacentElement(position, element)
insertAdjacentHTML은 string을 인자로 받고 insertAdjacentElement는 실제 Element 요소를 받아 처리한다는 차이가 있음!
* 관련 블로그
https://velog.io/@khw970421/InsertAdjacentHTML-vs-InsertAdjacentElement
chatroomId가 있는 tr태그를 찾고 insert 처리하므로 새로 개설된 채팅방 랜더링 시 오류 발생!!! 따라서 분기처리가 필요합니다.
chatList.jsp
<script>
const trClickHandler = (e) => {
const chatroomId = e.target.parentElement.dataset.chatroomId;
console.log(chatroomId);
// 팝업
const url = `${pageContext.request.contextPath}/admin/chat.do?chatroomId=\${chatroomId}`;
const name = chatroomId; // window의 이름으로 사용
const spec = "width=500px, height=500px";
open(url, name, spec);
};
// 채팅방 별 채팅 팝업 생성
document.querySelectorAll("tr[data-chatroom-id]").forEach((tr) => {
tr.addEventListener('click', trClickHandler);
});
// 채팅 목록 및 메세지 최신화 처리를 위한 구독
setTimeout(() => {
stompClient.subscribe('/app/admin/chatList', (message) => {
console.log(message);
const {chatroomId, memberId, msg} = JSON.parse(message.body);
let tr = document.querySelector(`tr[data-chatroom-id="\${chatroomId}"]`);
if(tr) {
tr.querySelector(".msg").innerHTML = msg;
} else {
// 신규채팅방인 경우
tr = document.createElement("tr");
const memberIdTd = document.createElement("td");
memberIdTd.innerHTML = memberId;
const msgTd = document.createElement("td");
msgTd.classList.add('msg');
msgTd.innerHTML = msg;
tr.append(memberIdTd, msgTd);
tr.addEventListener('click', trClickHandler);
}
// 끌어올리기
// 새로 생성된 것이 아닌 기존 존재하는 태그이므로 잘라 붙여넣기 처리!
document.querySelector("#tbl-chat-list tbody").insertAdjacentElement('afterbegin', tr);
});
}, 500);
</script>
'Java > Spring' 카테고리의 다른 글
Spring) 관리자와 1:1 채팅 - 채팅 로그 DB 저장 및 채팅 내역 가져오기 (0) | 2022.10.11 |
---|---|
Spring) 관리자와 1:1 채팅 - 기본 흐름, 구독 처리 (0) | 2022.10.10 |
Spring) Web-Socket - Stomp (게시글 조회 알림 구현) (0) | 2022.09.15 |
Spring) Web-Socket - Stomp (전체/개별 공지사항 전송 구현) (0) | 2022.09.15 |
Spring) Spring-WebSocket - Stomp, Stomp 환경설정 및 흐름파악 (0) | 2022.09.14 |