안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
관리자와 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이 필요할 것으로 보입니다.
(다음 포스팅)
'Java > Spring' 카테고리의 다른 글
Spring) 관리자 1:1 채팅 - 관리자 (채팅 목록 랜더링, 회원 별 채팅창 팝업, 채팅 보내기, 끌어올리기 처리) (2) | 2022.10.11 |
---|---|
Spring) 관리자와 1:1 채팅 - 채팅 로그 DB 저장 및 채팅 내역 가져오기 (0) | 2022.10.11 |
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 |