본문 바로가기
Java/Spring

Spring) 관리자 1:1 채팅 - 관리자 (채팅 목록 랜더링, 회원 별 채팅창 팝업, 채팅 보내기, 끌어올리기 처리)

by 박채니 2022. 10. 11.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.

 

관리자 - 채팅 목록 불러오기

 

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

 

InsertAdjacentHTML vs InsertAdjacentElement

moving1은 insertAdjacentHTML을 사용하였고moving2는 insertAdjacentElement를 사용하였다. 해당 결과는 ab이와 같다. 콘솔에서는 각각 string object로 나타난다. string 형태를 받아오는 역할을 한다. 즉, 리터럴

velog.io

 

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>