void afterConnectionEstablished(WebSocketSession session)
// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
void handlerMessage(WebSocketSession session, WebSocketMessage message)
// 클라이언트로부터 메시지가 도착하면 실행
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
// 클라이언트와 연결이 종료되면 실행
void handleTransportError(WebSocketSession session, Throwable exception)
// 메시지 전송중 에러가 발생하면 실행
handlerTextMessage(WebSocketSession session, TextMessage message)
// 클라이언트로부터 텍스트 메시지를 받았을 때 실행
디스코드는 음성을 왔다갔다
채팅은 text 문자로 왔다갔다
누가 / 몇 번 채팅방 / 어떤 메시지 / 받는 사람
중복되면 안되니깐 set으로
synchronizedSet = 동기화하는 set, 객체를 반환하는 set
→ 줄서서 가져가는 것
→ 의도치 않은 충돌 방지를 위해 동기화 방법으로 진행
alt+shift+s 기본생성자 선택testSock 주소로 요청하면 testHandler가 처리한다
그리고 testHandler는 class= ~~
악수하면서 소매넣기 하는 방법
sockjs라는 라이브러리를 사용
✔️pom.xml
...
<!-- Jackson-databind -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
...
<!-- Spring WebSocket -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!--Java에서 JSON을 쉽게 다룰 수 있게 해주는 구글 라이브러리 -->
<!-- GSON 라이브러리 -->
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
...
✔️project(chatting(model(websocket))) → ChattingWebsocketHandler.java
public class ChattingWebsocketHandler extends TextWebSocketHandler{
private Logger logger = LoggerFactory.getLogger(ChattingWebsocketHandler.class);
@Autowired
private ChattingService service;
// WebSocketSession : 클라이언트 - 서버간 전이중통신을 담당하는 객체 (JDBC Connection과 유사)
// 클라이언트의 최초 웹소켓 요청 시 생성
private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<WebSocketSession>());
// synchronizedSet : 동기화된 Set 반환(HashSet은 기본적으로 비동기)
// -> 멀티스레드 환경에서 하나의 컬렉션에 여러 스레드가 접근하여 의도치 않은 문제가 발생되지 않게 하기 위해
// 동기화를 진행하여 스레드가 여러 순서대로 한 컬렉션에 순서대로 접근할 수 있게 변경
// afterConnectionEstablished - 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 연결 요청이 접수되면 해당 클라이언트와 통신을 담당하는 WebSocketSession 객체가 전달되어 옴
// 이를 필드에 선언해 준 sessions에 저장
sessions.add(session);
// logger.info("{}연결됨", session.getId());
// System.out.println(session.getId() + "연결됨");
}
// handlerTextMessage - 클라이언트로부터 텍스트 메시지를 받았을 때 실행
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 전달받은 내용은 JSON 형태의 String
logger.info("전달받은 내용 : " + message.getPayload());
// Jackson에서 제공하는 객체
// JSON String -> VO Object
ObjectMapper objectMapper = new ObjectMapper();
Message msg = objectMapper.readValue( message.getPayload(), Message.class);
// Message 객체 확인
System.out.println(msg);
// DB 삽입 서비스 호출
int result = service.insertMessage(msg);
if(result > 0 ) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd hh:mm");
msg.setSendTime(sdf.format(new Date()) );
// 전역변수로 선언된 sessions에는 접속중인 모든 회원의 세션 정보가 담겨 있음
for(WebSocketSession s : sessions) {
// WebSocketSession은 HttpSession의 속성을 가로채서 똑같이 가지고 있기 때문에
// 회원 정보를 나타내는 loginMember도 가지고 있음
// 로그인된 회원 정보 중 회원 번호 얻어오기
int loginMemberNo = ((Member)s.getAttributes().get("loginMember")).getMemberNo();
logger.debug("loginMemberNo : " + loginMemberNo);
// 로그인 상태인 회원 중 targetNo가 일치하는 회원에게 메시지 전달
if(loginMemberNo == msg.getTargetNo() || loginMemberNo == msg.getSenderNo()) {
s.sendMessage(new TextMessage(new Gson().toJson(msg)));
}
}
}
}
// afterConnectionClosed - 클라이언트와 연결이 종료되면 실행
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
// logger.info("{}연결끊김",session.getId());
}
}
✔️resources(spring(appServlet)) → servlet-context.xml
<!-- 웹 소켓 처리 클래스를 bean으로 등록 -->
<beans:bean id="testHandler"
class="edu.kh.project.main.model.websocket.TestWebsocketHandler"/>
<!-- 어떤 주소로 웹소켓 요청이 오면 세션을 가로챌지 지정 -->
<websocket:handlers>
<websocket:mapping handler="testHandler" path="/testSock/"/>
<!-- 요청 클라이언트의 세션을 가로채서 WebSocketSession에 넣어 주는 역할 -->
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
<!-- SockJS를 이용한 웹소켓 연결 요청임을 명시 -->
<websocket:sockjs/>
</websocket:handlers>
<beans:bean id="chatHandler" class="edu.kh.project.chatting.model.websocket.ChattingWebsocketHandler" />
<websocket:handlers>
<!-- websocket 매핑 주소 -->
<websocket:mapping handler="chatHandler" path="/chattingSock" />
<websocket:handshake-interceptors>
<!-- interceptor : http통신에서 request, response를 가로채는 역할 handshake-interceptors
: HttpSession에 있는 값을 가로채서 WebSocketSession 넣어주는 역할 -->
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>
servlet-cotext.xml에 websocket 처리 클래스 bean으로 등록하기
✔️webapp(resources(js)) → main.js
// -------------------------------------------------------------------------------
// 웹소켓 테스트
// 1. SockJS 라이브러리 추가
// 2. SockJS를 이용해서 클라이언트용 웹소켓 객체 생성
let testSock = new SockJS("/testSock/");
function sendMessage(name, str){
// 매개 변수를 JS에 저장
let obj={}; // 비어 있는 객체
obj.name= name; // 객체에 일치하는 key가 없다면 자동으로 추가
obj.str= str;
// console.log(obj);
// 웹소켓 연결된 곳으로 메시지 전달
testSock.send(JSON.stringify(obj));
//JS객체 -> JSON
}
// 웹소켓 객체(testSock)가 서버로부터 전달받은 메시지가 있을 경우
testSock.onmessage = e => {
// e : 이벤트 객체
// e.data : 전달받은 메시지(JSON)
let obj = JSON.parse(e.data); // JSON -> JS 객체
console.log(`보낸 사람 : ${obj.name} / ${obj.str}`);
}
✔️webapp(WEB-INF(views(chatting))) → chatting.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang=ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>채팅방</title>
<link rel="stylesheet" href="/resources/css/main-style.css">
<link rel="stylesheet" href="/resources/css/board/boardDetail-style.css">
<link rel="stylesheet" href="/resources/css/chatting/chatting-style.css">
<script src="https://kit.fontawesome.com/a2e8ca0ae3.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="../common/header.jsp"></jsp:include>
<button id="addTarget">추가</button>
<div id="addTargetPopupLayer" class="popup-layer-close">
<span id="closeBtn">×</span>
<div class="target-input-area">
<input type="search" id="targetInput" placeholder="닉네임 또는 이메일을 입력하세요" autocomplete="off">
</div>
<ul id="resultArea">
<%-- <li class="result-row" data-id="1">
<img class="result-row-img" src="/resources/images/user.png">
<span> <mark>유저</mark>일</span>
</li>
<li class="result-row" data-id="2">
<img class="result-row-img" src="/resources/images/user.png">
<span><mark>유저</mark>이</span>
</li>
<li class="result-row">일치하는 회원이 없습니다</li> --%>
</ul>
</div>
<div class="chatting-area">
<ul class="chatting-list">
<c:forEach var="room" items="${roomList}">
<li class="chatting-item" chat-no="${room.chattingNo}" target-no="${room.targetNo}">
<div class="item-header">
<c:if test="${not empty room.targetProfile}">
<img class="list-profile" src="${room.targetProfile}">
</c:if>
<c:if test="${empty room.targetProfile}">
<img class="list-profile" src="/resources/images/user.png">
</c:if>
</div>
<div class="item-body">
<p>
<span class="target-name">${room.targetNickName}</span>
<span class="recent-send-time">${room.sendTime}</span>
</p>
<div>
<p class="recent-message">${room.lastMessage}</p>
<c:if test="${room.notReadCount > 0}">
<p class="not-read-count">${room.notReadCount}</p>
</c:if>
</div>
</div>
</li>
</c:forEach>
</ul>
<div class="chatting-content">
<ul class="display-chatting">
<%-- <li class="my-chat">
<span class="chatDate">14:01</span>
<p class="chat">가나다라마바사</p>
</li>
<li class="target-chat">
<img src="/resources/images/user.png">
<div>
<b>이번유저</b> <br>
<p class="chat">
안녕하세요?? 반갑습니다.<br>
ㅎㅎㅎㅎㅎ
</p>
<span class="chatDate">14:05</span>
</div>
</li> --%>
</ul>
<div class="input-area">
<textarea id="inputChatting" rows="3"></textarea>
<button id="send">보내기</button>
</div>
</div>
</div>
</main>
<jsp:include page="../common/footer.jsp"></jsp:include>
<!--------------------------------------- sockjs를 이용한 WebSocket 구현을 위해 라이브러리 추가 ---------------------------------------------->
<!-- https://github.com/sockjs/sockjs-client -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script>
// 로그인한 회원 번호
const loginMemberNo = "${loginMember.memberNo}";
</script>
<script src="/resources/js/chatting/chatting.js"></script>
</body>
</html>
✔️webapp(resources(js)) → chatting.jsp
const addTarget = document.querySelector("#addTarget"); // 추가 버튼
const addTargetPopupLayer = document.querySelector("#addTargetPopupLayer"); // 팝업 레이어
const closeBtn = document.querySelector("#closeBtn"); // 닫기 버튼
const targetInput = document.querySelector("#targetInput"); // 사용자 검색
const resultArea = document.querySelector("#resultArea"); // 검색 결과
let selectChattingNo; // 선택한 채팅방 번호
let selectTargetNo; // 현재 채팅 대상
let selectTargetName; // 대상의 이름
let selectTargetProfile; // 대상의 프로필
// 검색 팝업 레이어 열기
addTarget.addEventListener("click", e => {
addTargetPopupLayer.classList.toggle("popup-layer-close");
targetInput.focus();
});
// 검색 팝업 레이어 닫기
closeBtn.addEventListener("click", e => {
addTargetPopupLayer.classList.toggle("popup-layer-close");
resultArea.innerHTML = "";
});
// 사용자 검색(ajax)
targetInput.addEventListener("input", e => {
const query = e.target.value.trim();
// 입력된 값이 없을 때
if(query.length == 0){
resultArea.innerHTML = ""; // 이전 검색 결과 비우기
return;
}
// 입력된 값이 있을 때
if(query.length > 0){
fetch("/chatting/selectTarget?query="+query)
.then(resp => resp.json())
.then(list => {
//console.log(list);
resultArea.innerHTML = ""; // 이전 검색 결과 비우기
if(list.length == 0){
const li = document.createElement("li");
li.classList.add("result-row");
li.innerText = "일치하는 회원이 없습니다";
resultArea.append(li);
}
for(let member of list){
// li요소 생성(한 행을 감싸는 요소)
const li = document.createElement("li");
li.classList.add("result-row");
li.setAttribute("data-id", member.memberNo);
// 프로필 이미지 요소
const img = document.createElement("img");
img.classList.add("result-row-img");
// 프로필 이미지 여부에 따른 src 속성 선택
if(member.profileImage == null) img.setAttribute("src", "/resources/images/user.png");
else img.setAttribute("src", member.profileImage);
let nickname = member.memberNickname;
let email = member.memberEmail;
const span = document.createElement("span");
span.innerHTML = `${nickname} ${email}`.replace(query, `<mark>${query}</mark>`);
// 요소 조립(화면에 추가)
li.append(img, span);
resultArea.append(li);
// li요소에 클릭 시 채팅방에 입장하는 이벤트 추가
li.addEventListener('click', chattingEnter);
}
})
.catch(err => console.log(err) );
}
});
// 채팅방 입장 또는 선택 함수
function chattingEnter(e){
console.log(e.target); // 실제 클릭된 요소
console.log(e.currentTarget); // 이벤트 리스트가 설정된 요소
const targetNo = e.currentTarget.getAttribute("data-id");
fetch("/chatting/enter?targetNo="+targetNo)
.then(resp => resp.text())
.then(chattingNo => {
console.log(chattingNo);
selectRoomList(); // 채팅방 목록 조회
setTimeout(()=>{
// 만약 채팅방 목록 중 이미 존재하는 채팅방이 있으면 클릭해서 입장
const itemList = document.querySelectorAll(".chatting-item")
for(let item of itemList) {
if(item.getAttribute("chat-no") == chattingNo){
item.focus();
item.click();
addTargetPopupLayer.classList.toggle("popup-layer-close");
targetInput.value = "";
resultArea.innerHTML = "";
return;
}
}
}, 200);
// 0.2초마다 채팅방 목록 다시 불러오기
})
.catch(err => console.log(err));
}
// 비동기로 채팅방 목록 조회
function selectRoomList(){
fetch("/chatting/roomList")
.then(resp => resp.json())
.then(roomList => {
console.log(roomList);
// 채팅방 목록 출력 영역 선택
const chattingList = document.querySelector(".chatting-list");
// 채팅방 목록 지우기
chattingList.innerHTML = "";
// 조회한 채팅방 목록을 화면에 추가
for(let room of roomList){
const li = document.createElement("li");
li.classList.add("chatting-item");
li.setAttribute("chat-no", room.chattingNo);
li.setAttribute("target-no", room.targetNo);
if(room.chattingNo == selectChattingNo){
li.classList.add("select");
}
// item-header 부분
const itemHeader = document.createElement("div");
itemHeader.classList.add("item-header");
const listProfile = document.createElement("img");
listProfile.classList.add("list-profile");
if(room.targetProfile == undefined)
listProfile.setAttribute("src", "/resources/images/user.png");
else
listProfile.setAttribute("src", room.targetProfile);
itemHeader.append(listProfile);
// item-body 부분
const itemBody = document.createElement("div");
itemBody.classList.add("item-body");
const p = document.createElement("p");
const targetName = document.createElement("span");
targetName.classList.add("target-name");
targetName.innerText = room.targetNickName;
const recentSendTime = document.createElement("span");
recentSendTime.classList.add("recent-send-time");
recentSendTime.innerText = room.sendTime;
p.append(targetName, recentSendTime);
const div = document.createElement("div");
const recentMessage = document.createElement("p");
recentMessage.classList.add("recent-message");
if(room.lastMessage != undefined){
recentMessage.innerHTML = room.lastMessage;
}
div.append(recentMessage);
itemBody.append(p,div);
// 현재 채팅방을 보고 있지 않고, 읽지 않은 메시지 개수가 0개 이상인 경우 -> 읽지 않은 메시지 개수 출력
if(room.notReadCount > 0 && room.chattingNo != selectChattingNo ){
const notReadCount = document.createElement("p");
notReadCount.classList.add("not-read-count");
notReadCount.innerText = room.notReadCount;
div.append(notReadCount);
}else{
// 현재 채팅방을 보고 있는 경우
// 비동기로 해당 채팅방 메시지를 읽음으로 표시
fetch("/chatting/updateReadFlag",{
method : "PUT",
headers : {"Content-Type": "application/json"},
body : JSON.stringify({"chattingNo" : selectChattingNo, "memberNo" : loginMemberNo})
})
.then(resp => resp.text())
.then(result => console.log(result))
.catch(err => console.log(err));
}
li.append(itemHeader, itemBody);
chattingList.append(li);
}
roomListAddEvent();
})
.catch(err => console.log(err));
}
// 채팅 메시지 영역
const display = document.getElementsByClassName("display-chatting")[0];
// 채팅방 목록에 이벤트를 추가하는 함수
function roomListAddEvent(){
const chattingItemList = document.getElementsByClassName("chatting-item");
for(let item of chattingItemList){
item.addEventListener("click", e => {
// 클릭한 채팅방의 번호 얻어오기
//const id = item.getAttribute("id");
//const arr = id.split("-");
// 전역변수에 채팅방 번호, 상대 번호, 상태 프로필, 상대 이름 저장
selectChattingNo = item.getAttribute("chat-no");
selectTargetNo = item.getAttribute("target-no");
selectTargetProfile = item.children[0].children[0].getAttribute("src");
selectTargetName = item.children[1].children[0].children[0].innerText;
if(item.children[1].children[1].children[1] != undefined){
item.children[1].children[1].children[1].remove();
}
// 모든 채팅방에서 select 클래스를 제거
for(let it of chattingItemList) it.classList.remove("select")
// 현재 클릭한 채팅방에 select 클래스 추가
item.classList.add("select");
// 비동기로 메시지 목록을 조회하는 함수 호출
selectChattingFn();
});
}
}
// 비동기로 메시지 목록을 조회하는 함수
function selectChattingFn() {
fetch("/chatting/selectMessage?"+`chattingNo=${selectChattingNo}&memberNo=${loginMemberNo}`)
.then(resp => resp.json())
.then(messageList => {
console.log(messageList);
// <ul class="display-chatting">
const ul = document.querySelector(".display-chatting");
ul.innerHTML = ""; // 이전 내용 지우기
// 메시지 만들어서 출력하기
for(let msg of messageList){
//<li>, <li class="my-chat">
const li = document.createElement("li");
// 보낸 시간
const span = document.createElement("span");
span.classList.add("chatDate");
span.innerText = msg.sendTime;
// 메시지 내용
const p = document.createElement("p");
p.classList.add("chat");
p.innerHTML = msg.messageContent; // br 태그 해석을 위해 innerHTML
// 내가 작성한 메시지인 경우
if(loginMemberNo == msg.senderNo){
li.classList.add("my-chat");
li.append(span, p);
}else{ // 상대가 작성한 메시지인 경우
li.classList.add("target-chat");
// 상대 프로필
// <img src="/resources/images/user.png">
const img = document.createElement("img");
img.setAttribute("src", selectTargetProfile);
const div = document.createElement("div");
// 상대 이름
const b = document.createElement("b");
b.innerText = selectTargetName; // 전역변수
const br = document.createElement("br");
div.append(b, br, p, span);
li.append(img,div);
}
ul.append(li);
display.scrollTop = display.scrollHeight; // 스크롤 제일 밑으로
// 최근에 온 메시지는 아래로 계속 쌓이니까!
}
})
.catch(err => console.log(err));
}
// ----------------------------------------------------------------------------------------------------------------
// sockjs를 이용한 WebSocket 구현
// 로그인이 되어 있을 경우에만
// /chattingSock 이라는 요청 주소로 통신할 수 있는 WebSocket 객체 생성
let chattingSock;
if(loginMemberNo != ""){
chattingSock = new SockJS("/chattingSock");
}
// 채팅 입력
const send = document.getElementById("send");
const sendMessage = () => {
const inputChatting = document.getElementById("inputChatting");
if (inputChatting.value.trim().length == 0) {
alert("채팅을 입력해주세요.");
inputChatting.value = "";
} else {
var obj = {
"senderNo": loginMemberNo,
"targetNo": selectTargetNo,
"chattingNo": selectChattingNo,
"messageContent": inputChatting.value,
};
console.log(obj)
// JSON.stringify() : 자바스크립트 객체를 JSON 문자열로 변환
chattingSock.send(JSON.stringify(obj));
inputChatting.value = "";
}
}
// 엔터 == 제출
// 쉬프트 + 엔터 == 줄바꿈
inputChatting.addEventListener("keyup", e => {
if(e.key == "Enter"){
if (!e.shiftKey) {
sendMessage();
}
}
})
// WebSocket 객체 chattingSock이 서버로부터 메시지를 통지받으면 자동으로 실행될 콜백 함수
chattingSock.onmessage = function(e) {
// 메소드를 통해 전달받은 객체값을 JSON객체로 변환해서 obj 변수에 저장
const msg = JSON.parse(e.data);
console.log(msg);
// 현재 채팅방을 보고있는 경우
if(selectChattingNo == msg.chattingNo){
const ul = document.querySelector(".display-chatting");
// 메시지 만들어서 출력하기
//<li>, <li class="my-chat">
const li = document.createElement("li");
// 보낸 시간
const span = document.createElement("span");
span.classList.add("chatDate");
span.innerText = msg.sendTime;
// 메시지 내용
const p = document.createElement("p");
p.classList.add("chat");
p.innerHTML = msg.messageContent; // br 태그 해석을 위해 innerHTML
// 내가 작성한 메시지인 경우
if(loginMemberNo == msg.senderNo){
li.classList.add("my-chat");
li.append(span, p);
}else{ // 상대가 작성한 메시지인 경우
li.classList.add("target-chat");
// 상대 프로필
// <img src="/resources/images/user.png">
const img = document.createElement("img");
img.setAttribute("src", selectTargetProfile);
const div = document.createElement("div");
// 상대 이름
const b = document.createElement("b");
b.innerText = selectTargetName; // 전역변수
const br = document.createElement("br");
div.append(b, br, p, span);
li.append(img,div);
}
ul.append(li)
display.scrollTop = display.scrollHeight; // 스크롤 제일 밑으로
}
selectRoomList();
}
// 문서 로딩 완료 후 수행할 기능
document.addEventListener("DOMContentLoaded", ()=>{
// 채팅방 목록에 클릭 이벤트 추가
roomListAddEvent();
// 보내기 버튼에 이벤트 추가
send.addEventListener("click", sendMessage);
});
✔️webapp(resources(css(chatting))) → chatting-style.css
/* 채팅방 생성 버튼 */
#openChatRoom {
width: 120px;
}
/* 채팅방 생성 모달 */
#chat-modal {
justify-content: center;
align-items: center;
}
.modal-body {
text-align: center;
background-color: white;
padding: 50px;
border-radius: 20px;
}
#chatRoomTitle {
width: 250px;
}
/* 추가 버튼 */
#addTarget{
margin: 30px 0 0 70px;
width: 100px;
height: 30px;
background-color: #455BA7;
border: none;
border-radius: 10px;
font-weight: bold;
color: white;
cursor: pointer;
}
/* 팝업 레이어 */
#addTargetPopupLayer{
position: fixed;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1200;
background-color: #f5f5f5;
border: 5px inset #455ba8;
width: 450px;
height: 600px;
}
.popup-layer-close{ display: none; }
#closeBtn{
position: absolute;
top: -15px;
right: -30px;
font-size: 30px;
cursor: pointer;
}
.target-input-area{
width: 100%;
height: 40px;
border-bottom: 2px solid black;
}
#targetInput{
width: 100%;
height: 100%;
outline: none;
border: none;
padding: 3px 10px;
font-size: 20px;
}
#resultArea{list-style: none;}
.result-row{
width: 100%;
height: 50px;
padding: 5px;
cursor: pointer;
display: flex;
align-items: center;
}
.result-row:hover{ background-color: #dadada;}
.result-row > *{
margin-right: 10px;
user-select: none;
}
.result-row-img{ width: 40px;}
/* 채팅창 영역 */
.chatting-area {
margin: auto;
height: 650px;
width: 1000px;
margin-top: 20px;
margin-bottom: 50px;
display: flex;;
}
/* 채팅 목록 */
.chatting-list{
width: 30%;
border : 1px solid black;
list-style: none;
overflow: auto;
}
.chatting-item{
height: 12%;
display: flex;
padding: 5px 0;
border-bottom: 1px solid black;
cursor: pointer;
}
.chatting-item * {
pointer-events: none;
}
.chatting-item > div{ height: 100%; text-align: center;}
.item-header{ width : 25%; }
.item-body{
width : 75%;
padding : 2px 0;
}
.item-body> p{
display: flex;
justify-content: space-between;
}
.chatting-item.select{
background-color: #ddd;
}
.list-profile{
max-width: 65px;
max-height: 65px;
}
.target-name{
font-size: 1.2em;
font-weight: bold;
}
.recent-send-time{
margin-right: 5px;
}
.item-body > div{
display: flex;
justify-content: space-between;
}
.recent-message{
width: 180px;
/* 여러 줄 말줄임 처리 */
white-space: normal;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* 줄바꿈 처리 */
word-break: break-all;
}
.not-read-count{
width: 25px;
height: 25px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background-color: red;
color: white;
font-size: 12px;
margin: 10px 10px 0 0;
}
/* 채팅 내용 */
.chatting-content{
width: 70%;
}
.display-chatting {
width: 100%;
height: 570px;
border: 1px solid black;
overflow: auto;
list-style: none;
padding: 10px 10px;
}
.display-chatting > li{
margin: 10px 0;
}
.target-chat{
display: flex;
align-items: flex-start;
}
.target-chat > img{
width: 50px;
margin-right: 5px;
}
.chat {
display: inline-block;
border-radius: 5px;
padding: 5px;
background-color: #eee;
text-align: left;
max-width: 500px;
word-break: break-all;
white-space: pre-wrap;
}
.input-area {
height: 80px;
width: 100%;
display: flex;
}
#inputChatting {
padding: 3px;
font-size: 1.3em;
width: 80%;
resize: none;
}
#send {
width: 20%;
}
.my-chat {
text-align: right;
}
.my-chat>p {
background-color: yellow;
}
.chatDate {
font-size: 9px;
}
#exit-area {
text-align: right;
margin-bottom: 10px;
}
.exit {
text-align: center;
}
.exit>p {
background-color: rgba(0, 0, 0, 0.3);
}
.chat-exit {
width: 100%;
text-align: center;
background-color: black;
color: white;
}
✔️webapp(WEB-INF(views(common))) → header.jsp
...
<%-- 로그인 했을 때 채팅 보여짐 --%>
<c:if test="${!empty loginMember}" >
<li><a href="/chatting">채팅</a></li>
</c:if>
...
✔️project(chatting(model(dto))) → ChattingRoom.java
package edu.kh.project.chatting.model.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ChattingRoom {
private int chattingNo; // 채팅방 번호
private String lastMessage; // 최근 메시지
private String sendTime; // 메시지 보낸 시간
private int targetNo; // 받는 회원 번호
private String targetNickName; // 받는 회원 닉네임
private String targetProfile; // 받는 회원 프로필 사진
private int notReadCount; // 읽지 않은 메시지 개수
}
✔️project(chatting(model(dto))) → Message.java
package edu.kh.project.chatting.model.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Message {
private int messageNo; // 메시지 번호
private String messageContent; // 메시지 내용
private String readFlag; // 읽음 여부
private int senderNo; // 보낸 회원 번호
private int targetNo; // 받는 회원 번호
private int chattingNo; // 채팅방 번호
private String sendTime; // 메시지 보낸 시간
}
✔️project(chatting(controller)) → ChattingController.java
@SessionAttributes({"loginMember"})
@Controller
public class ChattingController {
@Autowired
private ChattingService service;
// 채팅 페이지
@GetMapping("/chatting")
public String chatting(@SessionAttribute("loginMember") Member loginMember, Model model) {
// 현재 개설되어 있는 채팅방 목록 불러오기
List<ChattingRoom> roomList = service.selectRoomList(loginMember.getMemberNo());
model.addAttribute("roomList", roomList);
return "chatting/chatting";
}
// 채팅 상대 검색
@GetMapping(value="/chatting/selectTarget", produces="application/json; charset=UTF-8")
@ResponseBody
public List<Member> selectTarget(String query, @SessionAttribute("loginMember") Member loginMember){
Map<String, Object> map = new HashMap<>();
map.put("memberNo", loginMember.getMemberNo());
map.put("query", query);
return service.selectTarget(map);
}
// 채팅방 입장(없으면 생성)
@GetMapping("/chatting/enter")
@ResponseBody
public int chattingEnter(int targetNo, @SessionAttribute("loginMember") Member loginMember) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("targetNo", targetNo);
map.put("loginMemberNo", loginMember.getMemberNo());
int chattingNo = service.checkChattingNo(map);
// 받는 회원과 생성된 채팅방이 없을 경우 채팅방 번호 새로 생성
if(chattingNo == 0) {
chattingNo = service.createChattingRoom(map);
}
return chattingNo;
}
// 채팅방 목록 조회
@GetMapping(value="/chatting/roomList", produces="application/json; charset=UTF-8")
@ResponseBody
public List<ChattingRoom> selectRoomList(@SessionAttribute("loginMember") Member loginMember) {
return service.selectRoomList(loginMember.getMemberNo());
}
// 채팅 읽음 표시
@PutMapping("/chatting/updateReadFlag")
@ResponseBody
public int updateReadFlag(@RequestBody Map<String, Object> paramMap) {
return service.updateReadFlag(paramMap);
}
// 채팅방 번호에 해당하는 메시지 목록 조회
@GetMapping(value="/chatting/selectMessage", produces="application/json; charset=UTF-8")
@ResponseBody
public List<Message> selectMessageList(@RequestParam Map<String, Object> paramMap) {
return service.selectMessageList(paramMap);
}
}
✔️project(chatting(model(service))) → ChattingService.java
public interface ChattingService {
List<ChattingRoom> selectRoomList(int memberNo);
int checkChattingNo(Map<String, Integer> map);
int createChattingRoom(Map<String, Integer> map);
int insertMessage(Message msg);
int updateReadFlag(Map<String, Object> paramMap);
List<Message> selectMessageList(Map<String, Object> paramMap);
/** 채팅 상대 검색
* @param map
* @return memberList
*/
List<Member> selectTarget(Map<String, Object> map);
}
✔️project(chatting(model(service))) → ChattingServiceImpl.java
@Service
public class ChattingServiceImpl implements ChattingService{
@Autowired
private ChattingDAO dao;
@Override
public List<ChattingRoom> selectRoomList(int memberNo) {
return dao.selectRoomList(memberNo);
}
@Override
public int checkChattingNo(Map<String, Integer> map) {
return dao.checkChattingNo(map);
}
@Override
public int createChattingRoom(Map<String, Integer> map) {
return dao.createChattingRoom(map);
}
@Override
public int insertMessage(Message msg) {
msg.setMessageContent(Util.XSSHandling(msg.getMessageContent()));
return dao.insertMessage(msg);
}
@Override
public int updateReadFlag(Map<String, Object> paramMap) {
return dao.updateReadFlag(paramMap);
}
@Override
public List<Message> selectMessageList( Map<String, Object> paramMap) {
System.out.println(paramMap);
List<Message> messageList = dao.selectMessageList( Integer.parseInt( String.valueOf(paramMap.get("chattingNo") )));
if(!messageList.isEmpty()) {
int result = dao.updateReadFlag(paramMap);
}
return messageList;
}
// 채팅 상대 검색
@Override
public List<Member> selectTarget(Map<String, Object> map) {
return dao.selectTarget(map);
}
}
✔️project(chatting(model(dao))) → ChattingDAO.java
@Repository
public class ChattingDAO {
@Autowired
private SqlSessionTemplate sqlSession;
public List<ChattingRoom> selectRoomList(int memberNo) {
return sqlSession.selectList("chattingMapper.selectRoomList", memberNo);
}
public int checkChattingNo(Map<String, Integer> map) {
return sqlSession.selectOne("chattingMapper.checkChattingNo", map);
}
public int createChattingRoom(Map<String, Integer> map) {
int result = sqlSession.insert("chattingMapper.createChattingRoom", map);
int chattingNo = 0;
if(result > 0) chattingNo = (int)map.get("chattingNo");
return chattingNo;
}
public int insertMessage(Message msg) {
return sqlSession.insert("chattingMapper.insertMessage", msg);
}
public int updateReadFlag(Map<String, Object> paramMap) {
return sqlSession.update("chattingMapper.updateReadFlag", paramMap);
}
public List<Message> selectMessageList(int chattingNo) {
return sqlSession.selectList("chattingMapper.selectMessageList", chattingNo);
}
public List<Member> selectTarget(Map<String, Object> map) {
return sqlSession.selectList("chattingMapper.selectTarget", map);
}
}
✔️resources(mappers) → chatting-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="chattingMapper">
<resultMap type="ChattingRoom" id="chattingRoom_rm">
<id property="chattingNo" column="CHATTING_NO" />
<result property="lastMessage" column="LAST_MESSAGE" />
<result property="sendTime" column="SEND_TIME" />
<result property="targetNo" column="TARGET_NO" />
<result property="targetNickName" column="TARGET_NICKNAME" />
<result property="targetProfile" column="TARGET_PROFILE" />
<result property="notReadCount" column="NOT_READ_COUNT" />
</resultMap>
<resultMap type="Message" id="message_rm">
<id property="messageNo" column="MESSAGE_NO" />
<result property="messageContent" column="MESSAGE_CONTENT" />
<result property="readFlag" column="READ_FL" />
<result property="senderNo" column="SENDER_NO" />
<result property="chattingNo" column="CHATTING_NO" />
<result property="sendTime" column="SEND_TIME" />
</resultMap>
<resultMap type="Member" id="member_rm">
<id property="memberNo" column="MEMBER_NO"/>
<result property="memberEmail" column="MEMBER_EMAIL"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="profileImage" column="PROFILE_IMG"/>
</resultMap>
<!--=========================================================================================-->
<!-- 채팅방 목록 조회 -->
<select id="selectRoomList" resultMap="chattingRoom_rm">
SELECT CHATTING_NO
,(SELECT MESSAGE_CONTENT FROM (
SELECT * FROM MESSAGE M2
WHERE M2.CHATTING_NO = R.CHATTING_NO
ORDER BY MESSAGE_NO DESC)
WHERE ROWNUM = 1) LAST_MESSAGE
,TO_CHAR(NVL((SELECT MAX(SEND_TIME) SEND_TIME
FROM MESSAGE M
WHERE R.CHATTING_NO = M.CHATTING_NO), CH_CREATE_DATE),
'YYYY.MM.DD') SEND_TIME
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_NO = R.CHATTING_NO
AND R2.OPEN_MEMBER = #{memberNo}),
R.PARTICIPANT,
R.OPEN_MEMBER
) TARGET_NO
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_NO = R.CHATTING_NO
AND R2.OPEN_MEMBER = #{memberNo}),
(SELECT MEMBER_NICKNAME FROM MEMBER WHERE MEMBER_NO = R.PARTICIPANT),
(SELECT MEMBER_NICKNAME FROM MEMBER WHERE MEMBER_NO = R.OPEN_MEMBER)
) TARGET_NICKNAME
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_NO = R.CHATTING_NO
AND R2.OPEN_MEMBER = #{memberNo}),
(SELECT PROFILE_IMG FROM MEMBER WHERE MEMBER_NO = R.PARTICIPANT),
(SELECT PROFILE_IMG FROM MEMBER WHERE MEMBER_NO = R.OPEN_MEMBER)
) TARGET_PROFILE
,(SELECT COUNT(*) FROM MESSAGE M WHERE M.CHATTING_NO = R.CHATTING_NO AND READ_FL = 'N' AND SENDER_NO != #{memberNo}) NOT_READ_COUNT
,(SELECT MAX(MESSAGE_NO) SEND_TIME FROM MESSAGE M WHERE R.CHATTING_NO = M.CHATTING_NO) MAX_MESSAGE_NO
FROM CHATTING_ROOM R
WHERE OPEN_MEMBER = #{memberNo}
OR PARTICIPANT = #{memberNo}
ORDER BY MAX_MESSAGE_NO DESC NULLS LAST
</select>
<!-- 채팅 확인 -->
<select id="checkChattingNo" resultType="_int">
SELECT NVL(SUM(CHATTING_NO),0) CHATTING_NO FROM CHATTING_ROOM
WHERE (OPEN_MEMBER = #{loginMemberNo} AND PARTICIPANT = #{targetNo})
OR (OPEN_MEMBER = #{targetNo} AND PARTICIPANT = #{loginMemberNo})
</select>
<!-- 채팅방 생성 -->
<insert id="createChattingRoom" parameterType="map" useGeneratedKeys="true">
<selectKey keyProperty="chattingNo" order="BEFORE" resultType="_int">
SELECT SEQ_ROOM_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO CHATTING_ROOM
VALUES(#{chattingNo}, DEFAULT, #{loginMemberNo}, #{targetNo})
</insert>
<!-- 채팅 메시지 삽입 -->
<insert id="insertMessage">
INSERT INTO "MESSAGE"
VALUES(SEQ_MESSAGE_NO.NEXTVAL, #{messageContent}, DEFAULT, DEFAULT, #{senderNo}, #{chattingNo})
</insert>
<!-- 채팅 메시지 중 내가 보내지 않은 글을 읽음으로 표시 -->
<update id="updateReadFlag">
UPDATE "MESSAGE" SET
READ_FL = 'Y'
WHERE CHATTING_NO = #{chattingNo}
AND SENDER_NO != #{memberNo}
</update>
<!-- 채팅방 메시지 조회 -->
<select id="selectMessageList" resultMap="message_rm">
SELECT MESSAGE_NO, MESSAGE_CONTENT, READ_FL, SENDER_NO, CHATTING_NO,
TO_CHAR(SEND_TIME, 'YYYY.MM.DD HH24:MI') SEND_TIME
FROM MESSAGE
WHERE CHATTING_NO = #{chattingNo}
ORDER BY MESSAGE_NO
</select>
<!-- 채팅 상대 검색 -->
<select id="selectTarget" resultMap="member_rm">
SELECT MEMBER_NO, MEMBER_EMAIL, MEMBER_NICKNAME, PROFILE_IMG FROM "MEMBER"
WHERE (MEMBER_EMAIL LIKE '%${query}%' OR MEMBER_NICKNAME LIKE '%${query}%')
AND MEMBER_DEL_FL = 'N'
AND MEMBER_NO != ${memberNo}
</select>
</mapper>
속성 마음대로 추가 가능
setTimeOut으로 동기식으로 바꿔준다
시간 차가 있기 때문에 동기식으로 해주는 것
-- 채팅
CREATE TABLE "CHATTING_ROOM" (
"CHATTING_NO" NUMBER NOT NULL,
"CH_CREATE_DATE" DATE DEFAULT SYSDATE NOT NULL,
"OPEN_MEMBER" NUMBER NOT NULL,
"PARTICIPANT" NUMBER NOT NULL
);
COMMENT ON COLUMN "CHATTING_ROOM"."CHATTING_NO" IS '채팅방 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."CH_CREATE_DATE" IS '채팅방 생성일';
COMMENT ON COLUMN "CHATTING_ROOM"."OPEN_MEMBER" IS '개설자 회원 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."PARTICIPANT" IS '참여자 회원 번호';
ALTER TABLE "CHATTING_ROOM" ADD CONSTRAINT "PK_CHATTING_ROOM" PRIMARY KEY (
"CHATTING_NO"
);
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_OPEN_MEMBER"
FOREIGN KEY ("OPEN_MEMBER") REFERENCES "MEMBER";
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_PARTICIPANT"
FOREIGN KEY ("PARTICIPANT") REFERENCES "MEMBER";
DROP TABLE "MESSAGE";
CREATE TABLE "MESSAGE" (
"MESSAGE_NO" NUMBER NOT NULL,
"MESSAGE_CONTENT" VARCHAR2(4000) NOT NULL,
"READ_FL" CHAR DEFAULT 'N' NOT NULL,
"SEND_TIME" DATE DEFAULT SYSDATE NOT NULL,
"SENDER_NO" NUMBER NOT NULL,
"CHATTING_NO" NUMBER NOT NULL
);
COMMENT ON COLUMN "MESSAGE"."MESSAGE_NO" IS '메시지 번호';
COMMENT ON COLUMN "MESSAGE"."MESSAGE_CONTENT" IS '메시지 내용';
COMMENT ON COLUMN "MESSAGE"."READ_FL" IS '읽음 여부';
COMMENT ON COLUMN "MESSAGE"."SEND_TIME" IS '메시지 보낸 시간';
COMMENT ON COLUMN "MESSAGE"."SENDER_NO" IS '보낸 회원의 번호';
COMMENT ON COLUMN "MESSAGE"."CHATTING_NO" IS '채팅방 번호';
ALTER TABLE "MESSAGE" ADD CONSTRAINT "PK_MESSAGE" PRIMARY KEY (
"MESSAGE_NO"
);
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_CHATTING_NO"
FOREIGN KEY ("CHATTING_NO") REFERENCES "CHATTING_ROOM";
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_SENDER_NO"
FOREIGN KEY ("SENDER_NO") REFERENCES "MEMBER";
-- 시퀀스 생성
CREATE SEQUENCE SEQ_ROOM_NO NOCACHE;
CREATE SEQUENCE SEQ_MESSAGE_NO NOCACHE;