웹 소켓으로 채팅 기능 구현하기 (23.09.04)

·2023년 9월 4일
1

Spring

목록 보기
32/36
post-thumbnail

🌷 웹소켓 (WebSocket)

브라우저와 웹 서버 간의 전이중통신을 지원하는 프로토콜

  • HTML5 버전부터 지원하는 기능임
  • 자바 톰캣7 버전부터 지원했으나 8 버전부터 본격적으로 지원하고 있음
  • spring4부터 웹소켓을 지원함

💡 전이중 통신(Full Duplex)

두 대의 단말기가 데이터를 송수신하기 위해 동시에 각각 독립된 회선을 사용하는 통신 방식
(대표적으로 전화망, 고속 데이터 통신이 있음)

🌼 WebSocketHandler 인터페이스

웹소켓을 위한 메소드를 지원하는 인터페이스
-> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해 웹소켓 기능을 구현

🌱 주요 메소드

void afterConnectionEstablished(WebSocketSession session)
// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
    
void handlerMessage(WebSocketSession session, WebSocketMessage message)
// 클라이언트로부터 메시지가 도착하면 실행
    
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
// 클라이언트와 연결이 종료되면 실행

void handleTransportError(WebSocketSession session, Throwable exception)
// 메시지 전송중 에러가 발생하면 실행 

🌼 TextWebSocketHandler

WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메시지 전용 웹소켓 핸들러 클래스

🌱 주요 메소드

handlerTextMessage(WebSocketSession session, TextMessage message)
// 클라이언트로부터 텍스트 메시지를 받았을 때 실행

🌷 채팅 기능

오늘은 웹소켓 기능을 활용하여 채팅 기능을 구현해 보려고 한다.
단계별로 차근차근 나아가 보자. 😉

🌼 Step 1

Maven Repository에서 Spring Websocket 다운로드 받기

  1. Maven Repository ▶ Spring WebSocket 검색 후 원하는 버전 다운로드

  2. pom.xml에 붙여넣기

채팅 기능을 구현하기 위해 Spring WebSocket와 더불어 Jackson-databind, GSON 라이브러리도 추가해 보자!

🔎 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>
...

🌼 Step 2

TextWebSocketHandler상속받는 클래스 만들기

클래스를 생성한 뒤 아래 세 가지 메소드를 구현해야 한다.

💡 afterConnectionClosed(WebSocketSession, CloseStatus)

클라이언트와 연결이 종료되면 실행

💡 afterConnectionEstablished(WebSocketSession)

클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행

💡 handleTextMessage(WebSocketSession, TextMessage)

클라이언트로부터 텍스트 메시지를 받았을 때 실행

클래스 생성한 뒤 Alt+Shift+s ▶ Override/implement Methods 클릭 ▶ afterConnectionClosed(...), afterConnectionExtablished(...), handleTextMessage(...) 선택 후 OK 버튼 클릭

🔎 ChattingWebsocketHandler.java

package edu.kh.project.chatting.model.websocket;

import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;

import edu.kh.project.chatting.model.dto.Message;
import edu.kh.project.chatting.model.service.ChattingService;
import edu.kh.project.member.model.dto.Member;


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());
    }
    
}

🌼 Step 3

servlet-cotext.xmlwebsocket 처리 클래스 bean으로 등록하기

🔎 servlet-context.xml

servlet-cotext.xml ▶ Namespaces 탭 ▶ websocket 선택 후 저장

	<!-- 웹 소켓 처리 클래스를 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> 

🌼 Step 4

DB에 채팅 관련 테이블 생성하기

-- 채팅
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;

SQL 구문을 실행하면 위와 같이 CHATTING_ROOM, MESSAGE 테이블이 만들어진다.


🌼 Step 5

VS Code에서 프론트엔드 구현하기

🔎 chatting.jsp

sockjs를 이용하여 WebSocket을 구현하기 위해서는 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">&times</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>

🔎 chatting.js

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);
});

🔎 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;
 }

🔎 header.jsp

로그인 했을 때만 header의 nav에 '채팅' 메뉴가 활성화되도록 코드를 작성해 주자.

...
        <%-- 로그인 했을 때 채팅 보여짐 --%>
        <c:if test="${!empty loginMember}" >
            <li><a href="/chatting">채팅</a></li>
        </c:if>
...

🌼 Step 6

Spring에서 백엔드 구현하기

Package Explorer로 본 구조는 위와 같다.

🔎 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; // 읽지 않은 메시지 개수
    
}

🔎 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; // 메시지 보낸 시간
}

🔎 ChattingController.java

package edu.kh.project.chatting.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;

import edu.kh.project.chatting.model.dto.ChattingRoom;
import edu.kh.project.chatting.model.dto.Message;
import edu.kh.project.chatting.model.service.ChattingService;
import edu.kh.project.member.model.dto.Member;

@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);
    }
    
}

🔎 ChattingService.java

package edu.kh.project.chatting.model.service;

import java.util.List;
import java.util.Map;

import edu.kh.project.chatting.model.dto.ChattingRoom;
import edu.kh.project.chatting.model.dto.Message;
import edu.kh.project.member.model.dto.Member;

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);

}

🔎 ChattingServiceImpl.java

package edu.kh.project.chatting.model.service;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import edu.kh.project.chatting.model.dao.ChattingDAO;
import edu.kh.project.chatting.model.dto.ChattingRoom;
import edu.kh.project.chatting.model.dto.Message;
import edu.kh.project.common.utility.Util;
import edu.kh.project.member.model.dto.Member;

@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);
   }
    
}

🔎 ChattingDAO.java

package edu.kh.project.chatting.model.dao;

import java.util.List;
import java.util.Map;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.chatting.model.dto.ChattingRoom;
import edu.kh.project.chatting.model.dto.Message;
import edu.kh.project.member.model.dto.Member;

@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);
   }
}

🔎 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>

🌼 Step 7

잘 구현되었는지 서버 실행해서 테스트! 👀

로그인을 하면 nav에 '채팅' 메뉴가 활성화된다.
클릭하고 들어가 보자!

현재는 채팅방이 하나도 존재하지 않는다. 대화 상대를 추가하여 채팅방을 개설해 보자.
좌측 상단에 '추가' 버튼을 클릭한다.

채팅하기를 희망하는 유저의 정보를 입력하면 검색어가 포함된 유저를 조회할 수 있다. 그중 유저일을 선택해 보자.

왼쪽 채팅방 목록에 '유저일'의 채팅방이 개설되었다. 메시지를 한번 보내 보자.
이제 마지막으로 유저일로 로그인하여 진짜로 메시지가 왔는지 확인해 볼까?

진짜로 유저오로부터 메시지가 와 있다!!
아직 읽지 않은 메시지는 메시지 수만큼 알림이 뜬다.

채팅방에 들어가면 나에게 온 메시지를 확인할 수 있다. 또한 상대방에게 메시지 전송도 가능하다. 👍


파이널 프로젝트에 채팅 기능을 꼭 넣어 보고 싶었는데 오늘 포스팅에서 작성한 내용을 참고하여 구현해 봐야겠다!

profile
풀스택 개발자 기록집 📁

0개의 댓글