코딩 노트
DM을 어떻게 보내는가 본문
전체 메세지일 경우 hello
보낼 때도 JSON처리 (json의 기본 규칙: 이름이 있어야 한다.)
{"content":"hello"}
DM일 경우 /w target hello
DM일 때도 JSOM으로 처리
{"target":"대상의ID", "content":"hello"}
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;
if(data.dm == true) {//DM이라면
if(data.target) {//target이 있다면 (내가 DM을 보내서 찍히는 메세지라면)
memberId = $("<strong>").text(data.target + " 님에게 보낸 DM");
}
else {//target이 없다면 (내가 DM을 받아서 찍히는 메세지라면)
memberId = $("<strong>").text(data.memberId + " 님으로부터의 DM");
}
}
else {//DM이 아니라면
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);
}
};
//메세지를 전송하는 코드
//- 메세지가 @로 시작하면 DM으로 처리(아이디 유무 검사정도 하면 좋고)
//- @아이디 메세지
$(".send-btn").click(function(){
var text = $(".message-input").val();
if(text.length == 0) return;
//window.socket.send(text);//일반 텍스트 형식으로 보낼 때
if(text.startsWith("@")) {//@로 시작하면
var space = text.indexOf(" ");
if(space == -1) return;
var obj = {
target: text.substring(1, space),
content: text.substring(space+1)
};
var str = JSON.stringify(obj);//객체를 JSON 문자열로 변환
window.socket.send(str);//JSON 형식으로 보낼 때
$(".message-input").val("");
}
else {
var obj = {
content:text
};
var str = JSON.stringify(obj);//객체를 JSON 문자열로 변환
window.socket.send(str);//JSON 형식으로 보낼 때
$(".message-input").val("");
}
});
//.btn-userlist를 누르면 사용자 목록에 active를 붙였다 떼었다 하도록 처리
$(".btn-userlist").click(function(){
$(".client-list").toggleClass("active");
});
</script>
</body>
</html>
ChatDto 생성
@Data @NoArgsConstructor @AllArgsConstructor @Builder
public class ChatDto {
private int chatNo;
private String chatSender, chatSenderLevel, chatReceiver;
private String chatContent;
private Date chatTime;
}
ChatDao 생성
public interface ChatDao {
void insert(ChatDto dto);
List<ChatDto> list();
}
ChatDaoImpl 생성
@Repository
public class ChatDaoImpl implements ChatDao{
@Autowired
private SqlSession sqlSession;
@Override
public void insert(ChatDto dto) {
sqlSession.insert("chat.add", dto);
}
@Override
public List<ChatDto> list() {
return sqlSession.selectList("chat.list");
}
chat-mapper.xml 생성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="chat">
<insert id="add">
insert into chat(chat_no, chat_content, chat_sender, chat_sender_level, chat_receiver)
values(
chat_seq.nextval,
#{chatContent},
#{chatSender},
#{chatSenderLevel},
#{chatReceiver}
)
</insert>
<select id="list" resultType="ChatDto">
select * from chat order by chat_no asc
</select>
</mapper>
properties 수정
# project setting file
# key=value
#sever setting
#server.servlet.context-path=/khacademy
##server.port=9999
#view resolver setting
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
# database setting
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=C##home
spring.datasource.password=home
spring.datasource.hikari.data-source-properties.oracle.jdbc.timezoneAsRegion=false
# mybatis setting
mybatis.type-aliases-package=com.kh.spring20.dto,com.kh.spring20.vo
mybatis.mapper-locations=/mybatis/**/*-mapper.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.jdbc-type-for-null=VARCHAR
#logging setting
logging.level.root=warn
logging.level.com.kh=debug
#logging.level.member=debug
logging.level.chat=debug
logging.pattern.console=[%-5level] %msg - %c [%d{yyyy-MM-dd HH:mm:ss.S}] %n
# logging file setting
#logging.file.name= logs/server.log
#logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss.S} [%-5level] %msg - %c %n
#logging.logback.rollingpolicy.max-file-size=10MB
#logging.logback.rollingpolicy.file-name-pattern=${LOG_FILE}-%d{yyyy-MM-dd-HH}-%i.log
#custom properties setting
#custom.fileupload.home=C:/upload/profile
#email setting
#custom.email.host=smtp.gmail.com
#custom.email.port=587
#custom.email.username=qufquf12120
#custom.email.password=ajeprfgyoraxwuks
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<>();//로그인한 회원
//JSON 변환기
private ObjectMapper mapper = new ObjectMapper();
@Autowired
private ChatDao chatDao;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
ClientVO client = new ClientVO(session);
clients.add(client);
if(client.isMember()) {
members.add(client);
}
log.debug("사용자 접속! 현재 {}명", clients.size());
log.debug("접속한 사용자 = {}", client);
//모든 접속자에게 접속자 명단을 전송
sendClientList();
sendMessageList(client);
}
@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);
}
}
//접속한 사용자에게 메세지 이력을 전송하는 메소드
public void sendMessageList(ClientVO client) throws IOException {
List<ChatDto> list = chatDao.list();
for(ChatDto dto : list) {
//dto의 내용을 메세지 형식으로 만들어서 전송
//- dto에 chatReceiver가 있으면 DM, 없으면 DM이 아님
//- 시간은 FE 미구현으로 첨부하지 않음
if(dto.getChatReceiver() == null) {//전체채팅인 경우 - chatReceiver가 null인 경우
Map<String, Object> map = new HashMap<>();
map.put("content", dto.getChatContent());
map.put("memberId", dto.getChatSender());
map.put("memberLevel", dto.getChatSenderLevel());
String messageJson = mapper.writeValueAsString(map);
TextMessage message = new TextMessage(messageJson);
client.send(message);
}
else {//DM이라면
if(client.isMember() == false)
continue;//비회원 컷트
if(client.getMemberId().equals(dto.getChatSender()) == false &&
client.getMemberId().equals(dto.getChatReceiver()) == false)
continue;//작성자나 수신자가 아니면(제3자인 경우) 컷트
//접속한 사람이 보낸 메세지라면 5개의 데이터를 전송(dm, memberId, memberLevel, content, target)
if(client.getMemberId().equals(dto.getChatSender())) {
Map<String, Object> map = new HashMap<>();
map.put("content", dto.getChatContent());
map.put("memberId", dto.getChatSender());
map.put("memberLevel", dto.getChatSenderLevel());
map.put("dm", true);
map.put("target", dto.getChatReceiver());
String messageJson = mapper.writeValueAsString(map);
TextMessage message = new TextMessage(messageJson);
client.send(message);
}
else {//접속한 사람이 받은 메세지라면 4개의 데이터를 전송
Map<String, Object> map = new HashMap<>();
map.put("content", dto.getChatContent());
map.put("memberId", dto.getChatSender());
map.put("memberLevel", dto.getChatSenderLevel());
map.put("dm", true);
String messageJson = mapper.writeValueAsString(map);
TextMessage message = new TextMessage(messageJson);
client.send(message);
}
}
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//사용자가 보낸 메세지를 모두에게 broadcast
ClientVO client = new ClientVO(session);
if(client.isMember() == false) return;
//(+추가) 사용자는 메세지를 JSON 형태로 보내므로 이를 해석해야 한다(ObjectMapper)
Map params = mapper.readValue(message.getPayload(), Map.class);
//log.debug("params = {}", params);
//log.debug("DM인가요 = {}", params.get("target") != null);
//DM일 경우와 아닐 경우를 구분하여 처리
boolean isDM = params.get("target") != null;
if(isDM) {//DM일 경우
//정보를 Map에 담아서 변환 후 전송
Map<String, Object> map = new HashMap<>();
map.put("dm", true);
map.put("memberId", client.getMemberId());
map.put("memberLevel", client.getMemberLevel());
map.put("content", params.get("content"));
//시간 추가 등
String messageJson = mapper.writeValueAsString(map);
TextMessage tm = new TextMessage(messageJson);
for(ClientVO c : members) {
if(c.getMemberId().equals(params.get("target"))) {//내가 찾던 사람이라면
c.send(tm);//대상에게 메세지 전송
}
}
//수신자에게 target 항목을 추가하여 다시 메세지 전송
map.put("target", params.get("target"));
messageJson = mapper.writeValueAsString(map);
tm = new TextMessage(messageJson);
client.send(tm);//작성자에게 메세지 전송
//DB insert (DM일 경우 내용,발신자,발신자등급,수신자를 저장)
chatDao.insert(ChatDto.builder()
.chatContent((String)params.get("content"))
.chatSender(client.getMemberId())
.chatSenderLevel(client.getMemberLevel())
.chatReceiver((String)params.get("target"))
.build());
}
else {//전체 채팅일 경우
//정보를 Map에 담아서 변환 후 전송
Map<String, Object> map = new HashMap<>();
map.put("memberId", client.getMemberId());
map.put("memberLevel", client.getMemberLevel());
map.put("content", params.get("content"));
//시간 추가 등
String messageJson = mapper.writeValueAsString(map);
TextMessage tm = new TextMessage(messageJson);
//메세지 발송
for(ClientVO c : clients) {
c.send(tm);
}
//DB insert (전체 메세지일 경우 내용,발신자,발신자등급을 저장)
chatDao.insert(ChatDto.builder()
.chatContent((String)params.get("content"))
.chatSender(client.getMemberId())
.chatSenderLevel(client.getMemberLevel())
.build());
}
}
}
'Spring' 카테고리의 다른 글
결제02 - 단건결제 (0) | 2023.10.25 |
---|---|
결제01 - (카카오페이 api) + 카멜케이스 변환 (0) | 2023.10.24 |
웹소켓 - 실시간 채팅, 반응형 디자인 (0) | 2023.10.23 |
실시간 채팅(WebSocket) (0) | 2023.10.20 |
HTTP 쿠키(Cookie) (0) | 2023.10.18 |