본문 바로가기
Java/Spring

Spring) 관리자와 1:1 채팅 - 기본 흐름, 구독 처리

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

 

관리자와 1:1 채팅 구현

- 관리자와 1:1 채팅이므로 채팅 인원이 관리자(고정) + 로그인 회원으로 구성

- 채팅방 아이디와 회원 아이디를 복합 PK로 설정

- 채팅방 번호가 'abcde'인 채팅이 개설 되었을 때 DB에 데이터는 아래와 같음

chatroom_id member_id
abcde admin
abcde honggd

- 동일한 채팅방 번호를 가진 member_id가 두 행씩 들어가며 관리자인 'admin'은 고정!

 

DB

-- 관리자와 1:1 채팅
create table chat_member(
    chatroom_id varchar2(50),
    member_id varchar2(50),
    last_check number default 0, -- 채팅방에 언제 마지막으로 입장했는지
    created_at date default sysdate,
    deleted_at date,
    constraint pk_chat_member primary key(chatroom_id, member_id), -- 복합 PK
    constraint fk_chat_member_id foreign key(member_id) references member(member_id)
);  

create table chat_log(
    no number,
    chatroom_id varchar2(50),
    member_id varchar2(50),
    msg varchar2(4000),
    time number,
    constraint pk_chat_log_no primary key(no),
    constraint fk_chat_log foreign key(chatroom_id, member_id) references chat_member(chatroom_id, member_id)
);

 

Dto

ChatMember

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
public class ChatMember {
	@NonNull
	private String chatroomId;
	@NonNull
	private String memberId;
	private long lastCheck;
	private LocalDateTime createdAt;
	private LocalDateTime deletedAt;
}

 

ChatLog

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatLog {
	private long no;
	private String chatroomId;
	private String memberId;
	private String msg;
	private long time;
}

 

header.jsp

<sec:authorize access="isAuthenticated() && !hasRole('ADMIN')">
    <li class="nav-item"><a class="nav-link" href="${pageContext.request.contextPath}/chat/chat.do">관리자와 1:1 채팅</a></li>
</sec:authorize>
<sec:authorize access="hasRole('ADMIN')">
    <li class="nav-item"><a class="nav-link" href="${pageContext.request.contextPath}/admin/memberList.do">관리자</a></li>
    <li class="nav-item"><a class="nav-link" href="${pageContext.request.contextPath}/admin/chatList.do">채팅목록</a></li>
</sec:authorize>

일반회원의 경우
관리자인 경우

관리자가 아닌 경우에는 채팅방으로 이동, 관리자인 경우에는 채팅 리스트로 이동하도록 하였습니다.


채팅방 접속

 

Controller

ChatController

@Controller
@Slf4j
@RequestMapping("/chat")
public class ChatController {
	@Autowired
	ChatService chatService;
	
	/**
	 * 1. 채팅방유무 조회
	 * 
	 * 2.a 처음 입장한 경우
	 * 		- 채팅방 아이디 생성
	 * 2.b 재입장인 경우
	 * 		- 기존 채팅방아이디
	 */
	@GetMapping("/chat.do")
	public void chat(Authentication authentication, Model model) {
		// 1. 채팅방 유무 조회
		Member loginMember = (Member)authentication.getPrincipal();
		ChatMember chatMember = chatService.findChatMemberByMemberId(loginMember.getMemberId());
		
		String chatroomId = null;
		if(chatMember == null) {
			// 처음 입장한 경우
			chatroomId = generateChatroomId();
			log.debug("chatroomId = {}", chatroomId);
			// chatmember insert 2행! (관리자/로그인회원)
			List<ChatMember> chatMembers = Arrays.asList(
						new ChatMember(chatroomId, loginMember.getMemberId()),
						new ChatMember(chatroomId, "admin") // ROLE로 admin을 구별하지만, 일단 'admin' 아이디로 고정!
					);
			chatService.insertChatMembers(chatMembers);
		} else {
			// 재입장한 경우
			chatroomId = chatMember.getChatroomId();
		}
		model.addAttribute("chatroomId", chatroomId);
	}

	private String generateChatroomId() {
		Random random = new Random();
		StringBuilder sb = new StringBuilder();
		final int len = 8;
		
		for(int i = 0; i < len; i++) {
			if(random.nextBoolean()) {
				// 영문자
				if(random.nextBoolean()) {
					// 대문자
					sb.append((char)(random.nextInt(26) + 'A')); // A-Z
				} else {
					// 소문자
					sb.append((char)(random.nextInt(26) + 'a')); // a-z
				}
			} else {
				// 숫자
				sb.append(random.nextInt(10)); // 0 ~ 9
			}
		}
		return sb.toString();
	}
}

 

Service (interface 생략)

ChatServiceImpl

@Service
@Transactional(rollbackFor = Exception.class)
public class ChatServiceImpl implements ChatService {
	@Autowired
	ChatDao chatDao;
	
	@Override
	public ChatMember findChatMemberByMemberId(String memberId) {
		return chatDao.findChatMemberByMemberId(memberId);
	}
	
	@Override
	public void insertChatMembers(List<ChatMember> chatMembers) {
		for(ChatMember member : chatMembers) {
			chatDao.insertChatMember(member);
		}
	}
}

 

Dao

ChatDao interface

public interface ChatDao {

	@Select("select * from chat_member where member_id = #{memberId}")
	ChatMember findChatMemberByMemberId(String memberId);
	
	@Insert("insert into chat_member values(#{chatroomId}, #{memberId}, default, default, default)")
	void insertChatMember(ChatMember member);

}

 

최초 접속 시

DB에 값이 저장 되는 것을 확인할 수 있습니다.

 

재입장 시

DEBUG: com.ce.spring2.common.interceptor.LogInterceptor - ==============================================
DEBUG: com.ce.spring2.common.interceptor.LogInterceptor - GET /spring2/chat/chat.do
DEBUG: com.ce.spring2.common.interceptor.LogInterceptor - ----------------------------------------------
DEBUG: com.ce.spring2.chat.model.dao.ChatDao.findChatMemberByMemberId - ==>  Preparing: select * from chat_member where member_id = ? 
DEBUG: com.ce.spring2.chat.model.dao.ChatDao.findChatMemberByMemberId - ==> Parameters: honggd(String)
DEBUG: com.ce.spring2.chat.model.dao.ChatDao.findChatMemberByMemberId - <==      Total: 1
DEBUG: com.ce.spring2.chat.controller.ChatController - chatMember = ChatMember(chatroomId=ytI81p26, memberId=honggd, lastCheck=0, createdAt=2022-10-10T21:19:53, deletedAt=null)

DB에 데이터 저장 되지 않고, ChatMember 객체를 가져오는 것을 확인할 수 있습니다.


채팅 구독처리

 

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" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<jsp:include page="/WEB-INF/views/common/header.jsp">
	<jsp:param value="관리자와 1:1채팅" name="title"/>
</jsp:include>

<div class="input-group mb-3">
  <input type="text" id="msg" 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"></ul>
</div>
<script>
setTimeout(() => {
	stompClient.subscribe(`/app/chat/${chatroomId}`, (message) => {
		console.log(`/app/chat/${chatroomId} : `, message);
	});
}, 500);
</script>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>

ws.js는 header.jsp에 선언되어있기 때문에 setTimeout()이 없다면, stomp는 비동기처리이므로 connect 호출 후 연결 이전에 subscribe처리되어 오류가 발생할 것입니다.

따라서 연결 후 subscribe()로 구독처리 될 수 있도록 setTimeout을 걸어주었습니다.

구독 처리, 메세지 전송/받음이 정상적으로 처리되는 것들을 확인할 수 있습니다.


채팅메세지 보내기

 

chat.jsp

<script>
setTimeout(() => {
	stompClient.subscribe(`/app/chat/${chatroomId}`, (message) => {
		console.log(`/app/chat/${chatroomId} : `, message);
		
		const {memberId, msg, time} = JSON.parse(message.body);
		const li = `
			<li class="list-group-item" title="\${time}">\${memberId} : \${msg}</li>
		`;
		const wrapper = document.querySelector("#data");
		wrapper.insertAdjacentHTML('beforeend', li);
	});
}, 500);

document.querySelector("#sendBtn").addEventListener('click', (e) => {
	const msg = document.querySelector("#msg").value;
	if(!msg) return;
	
	const chatlog = {
		chatroomId : `${chatroomId}`,
		memberId : '<sec:authentication property="principal.username"/>',
		msg,
		time : Date.now()
	};
	stompClient.send(`/app/chat/${chatroomId}`, {}, JSON.stringify(chatlog));
	// 초기화
	document.querySelector("#msg").value = '';
});
</script>

해당 메세지들을 DB에 저장하기 위해 MessageMapping이 필요할 것으로 보입니다.

(다음 포스팅)