본문 바로가기
개발/Spring

[SpringBoot] WebSocket 채팅방 구현(3) - html , js

by 코딩하는 흰둥이 2024. 8. 18.

이전글

https://greed-yb.tistory.com/281

 

[SpringBoot] WebSocket 채팅방 구현(2) - WebSocketConfig

Java 로 구현WebSocketConfigimport org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springf

greed-yb.tistory.com

 

 

html
<style>
    tr.selected {
        background-color: #f1f1f1; /* 선택된 행의 배경색 변경 */
    }

    /* 네비 창 */
    .sb-customizer {
        z-index: 1050;
        position: fixed;
        top: 4.5rem;
        width: 34rem;
        height: 90vh;
        text-align: left;
        background: #fff;
        transition: right .5s;
        box-shadow: -0.15rem 0 1.75rem 0 rgb(34 39 46 / 15%);
    }

    .sb-customizer.sb-customizer-open {
        right: 0;
    }

    .sb-customizer.sb-customizer-closed {
        right: -34rem;
    }

    .sb-customizer .sb-customizer-heading {
        padding: 1.0rem;
        font-size: 1.2rem;
        font-weight: 800;
        text-transform: uppercase;
        letter-spacing: .05em;
        color: #a7aeb8;
    }

    .sb-customizer .sb-customizer-content {
        position: relative;
        height: calc(100% - 8.5rem);
        /*height: 100%;*/
        overflow-y: scroll;
        padding: 0.5rem;
    }

    /*.sb-customizer .sb-customizer-toggler {*/
    /*    position: absolute;*/
    /*    !*top: 4.625rem;*!*/
    /*    display: flex;*/
    /*    align-items: center;*/
    /*    justify-content: center;*/
    /*    left: -3rem;*/
    /*    width: 3rem;*/
    /*    height: 3rem;*/
    /*    border-top-left-radius: 0.35rem;*/
    /*    border-bottom-left-radius: 0.35rem;*/
    /*    border-top-right-radius: 0;*/
    /*    border-bottom-right-radius: 0;*/
    /*    border: 0;*/
    /*    box-shadow: 0 0.15rem 1.75rem 0 rgb(34 39 46 / 15%);*/
    /*    color: #001500;*/
    /*    background-color: #fff;*/
    /*}*/


    #messenger_dataTable {
        width: 100% !important;
        table-layout: fixed;
    }

    #roomList_dataTable {
        width: 100% !important;
        table-layout: fixed;
    }

    .text-red-200 {
        color: #d62516 !important;
    }


    /*#messenger_dataTable td {*/
    /*    word-wrap: break-word;*/
    /*}*/
</style>

    
    
<!-- 메신저 창  -->
<div config-right id="config-switcher" class="sb-customizer sb-customizer-closed">
    <div config-right class="sb-customizer-heading shadow d-flex justify-content-end row m-0 p-1">
        <div id="add-switcher-title" class="col-md-6">
            <h6 class="mt-2 font-weight-bold text-primary float-left">메신저</h6>
        </div>
        <div class="col-md-6">
            <button onclick="cancelForm()" class="btn btn-danger btn-sm mx-1 float-right">닫기
            </button>
        </div>
    </div>
    <div id="messengerDiv" class="card-body mb-4" style="display: block">
        <div class="table-responsive">
            <table class="table table-bordered" id="messenger_dataTable">
            </table>
        </div>
        <div>
            <button onclick="openRoomList()" class="btn btn-dark btn-sm mx-1 float-right">대화방 목록
            </button>
        </div>
    </div>
    <div id="roomDiv" class="card-body mb-4" style="display: none">
        <div class="table-responsive">
            <table class="table table-bordered" id="roomList_dataTable">
            </table>
        </div>
        <div>
            <button onclick="backChatRoom()" class="btn btn-dark btn-sm mx-1 float-left">이전
            </button>
        </div>
    </div>
    <div id="chatRoomDiv" class="card-body mb-4" style="display: none">

        <div class="card-header py-3" style="text-align: left">
            <button class="btn btn-info btn-sm mx-1 float-left" onclick="openRoomList()()">이전</button>
            <h5 class="m-0 font-weight-bold text-dark text-center" id="chatRoomTitle"></h5>
        </div>

        <div class="card shadow mb-4">
            <div id="msgDiv"class="card-body" style="height: 600px; overflow: auto;">
                <div id="msgArea" class="row">
                </div>
            </div>
            <div>
                <div class="input-group mb-3">
                    <input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">
                    <div class="input-group-append">
                        <button class="btn btn-outline-secondary" type="button" id="send" onclick="sendMsg()">전송</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

 

구현 화면에 있는 전체 코드가 아닌 메신저 부분 코드만 올렸다

widgetWebSocket 으로 변경되는 top 부분 메시지 알람사내 메신저 부분은 빠져 있으며 js 만 올리려 한다 

 

style 의 sb-customizer  설정은 메신저 div 가 오른쪽에서 나오는 효과를 주고 있으며,

messenger_dataTable 과 roomList_dataTable 은 DataTable 크기를 고정하기 위함이다

 

Bootstrap 을 사용하지 않거나 또는 다른 버전을 사용하는 경우,

본인의 프로젝트에 맞게 style을 바꾸길 바란다

 

 

 

Javascript
// 메신저
let messenger_Info = [];
let messenger_dataTable;

// 방 목록
let roomList_Info = [];
let roomList_dataTable;

// 방 id
let roomId = null;
// 상대방 id
let targetId = null;


// 채팅방 웹소켓
let ws;

// widget 웹소켓
let ws_widget;
let ws_targetUser;


/**
 * 읽지 않은 메신저 글 수
 */
function messengerReadCount(){
    $.ajax({
        url: "/messengerReadCount",
        type: "GET",
        success: function (data) {
            if (data != null) {

                if(data > 0){
                    // 사내 메신저 카운트 및 이모티콘 색상 변경
                    $(".fa-comments").removeClass('text-gray-300')
                    $(".fa-comments").removeClass('text-red-200')
                    $(".fa-comments").addClass('text-red-200')

                    $("#alertSpan").remove();

                    // top.html 알람 표시
                    if(data > 3){
                        $("#alertsDropdown").append('<span id="alertSpan" class="badge badge-danger badge-counter">3+</span>');
                    }else{
                        $("#alertsDropdown").append('<span id="alertSpan" class="badge badge-danger badge-counter">' + data +'</span>');
                    }
                }else {

                    $("#alertSpan").remove();

                    // 사내 메신저 카운트 및 이모티콘 색상 변경
                    $(".fa-comments").removeClass('text-red-200')
                    $(".fa-comments").removeClass('text-gray-300')
                    $(".fa-comments").addClass('text-gray-300')

                    // top.html 알람 표시
                    $("#alertsDropdown").append('<span id="alertSpan"></span>');

                }
                // 사내 메신저 카운트 및 이모티콘 색상 변경
                $("#readCount").text(data);
            }
        },
        error(e) {
            console.log("error : " + e);
        }
    });
}


/**
 * 메신저 열고 닫기
 */
function openForm() {
    $('#config-switcher').removeClass('sb-customizer-closed');
    $('#config-switcher').addClass('sb-customizer-open');

    $("#messengerDiv").css("display","block");
    $("#chatRoomDiv").css("display","none");
    $("#roomDiv").css("display","none");

    // 유저 정보 가져오기
    messenger();
}
function cancelForm() {
    // 채팅방 id 초기화
    roomId = null;

    // 웹소켓 연결해제
    if(ws != null){
        ws.close();
    }

    $('#config-switcher').addClass('sb-customizer-closed');
    $('#config-switcher').removeClass('sb-customizer-open');
}



function messenger(){
    // 모든 유저
        $.ajax({
            url: "/messengerInfo",
            type: "GET",
            success: function (data) {
                if (data != null) {
                    messenger_Info = data;
                    messenger_Init(messenger_Info);
                }
            },
            error(e) {
                console.log("error : " + e);
            }
        });
}



/**
 * 메신저 유저 정보 DataTables 출력
 * @param data
 */
function messenger_Init(data){
        if ($.fn.DataTable.isDataTable('#messenger_dataTable')) {
            $('#messenger_dataTable').DataTable().destroy(); // 기존 인스턴스 제거
            // role_dataTable.destroy();
        }

    messenger_dataTable = $("#messenger_dataTable").DataTable({
            data: data,                                                     // 가져온 데이터
            "paging": false,                                                 // 페이징
            "searching": true,                                              // 검색 기능
            "info": false,                                                   // 정보 표시
            "ordering": false,                                               // 정렬 기능
            "scrollCollapse": true,
            "scrollY": '600px',
            "scrollX": "100%",
            "scrollXInner": "100px",
            "bAutoWidth": false,
            "language": {                                                   // 언어 설정
                decimal: "",
                emptyTable: "검색된 데이터가 없습니다.",
                info: "Showing _START_ to _END_ of _TOTAL_ entries",
                infoEmpty: "Showing 0 to 0 of 0 entries",
                infoFiltered: "(filtered from _MAX_ total entries)",
                infoPostFix: "",
                thousands: ",",
                lengthMenu: " _MENU_ ",
                loadingRecords: "Loading...",
                processing: "Processing...",
                search: "Search",
                zeroRecords: "항목이 존재하지 않습니다",
                paginate: {
                    "first": "처음",
                    "last": "마지막",
                    "next": "다음",
                    "previous": "이전"
                },
                aria: {
                    sortAscending: ": activate to sort column ascending",
                    sortDescending: ": activate to sort column descending"
                }
            },
            "columnDefs": [
                { width: "95%", targets : [0] }
            ],
            "columns": [
                {
                    data: null,
                    render: function (data, type, row) {
                        if(row.profile == null || row.profile == ''){
                            return '<img src="/uploadPath/image/topFile_cc49d4f3-5119-43b0-931e-5e7bb5aaa8df.jpg" id=" '+ data.id + '"/>'+data.name
                        }else{
                            return '<img src="/uploadPath/image/topFile_' + row.profile + '"' + 'id=" '+ data.id + '"/>'+data.name
                        }
                    }
                }
            ]
        });

        /* 선택된 tr 행 색상 변경 및 권한에 해당하는 메뉴 정보 가져오기*/
        $('#messenger_dataTable tbody').on('click', 'tr', function () {
            let tr = $(this).closest('tr');
            let row = messenger_dataTable.row(tr);

            if (tr.hasClass('selected')) {
                tr.removeClass('selected');

            } else {
                messenger_dataTable.$('tr.selected').removeClass('selected');//테이블에서 선택된 것 삭제
                tr.addClass('selected');

            }
        });

    // 리스너를 제거하고 다시 등록하는 과정으로 off 와 on 을 두어서 dbclick 할때 tr 행이 중복으로 눌리는 것을 방지한다
    $('#messenger_dataTable tbody').off('dblclick', 'tr').on('dblclick', 'tr', function () {
        let tr = $(this).closest('tr');
        let row = messenger_dataTable.row(tr);

        targetId = row.data().id;

            $.ajax({
            url: "/chatRoomInfo",
            type: "GET",
            data: {"targetId" : targetId , "userId" : user_info.id},
            success: function (data) {
                if (data != "") {
                    $("#msgArea").empty();
                    $("#chatRoomTitle").text(row.data().name);
                    $("#messengerDiv").css("display","none");
                    $("#chatRoomDiv").css("display","block");

                    roomId = data[0].roomId;

                    for (let i = 0; i < data.length; i++) {
                        if(data[i].sendUserId == user_info.id){
                            let str = "<div clas='col-12' style='width:100%'><div class='col-10' style='float: right'>";
                            str += "<div style='text-align: right; background-color: #f8f9fc;'>";
                            str += "<b>" + data[i].roomDetail + "</b>";
                            str += "</div></div></div>";
                            $("#msgArea").append(str);
                        }else{
                            let str = "<div class='col-12' style='width:100%'><div class='col-10' style='float: left'>";
                            str += "<div style='text-align: left'>";
                            str += "<b>" + data[i].roomDetail + "</b>";
                            str += "</div></div></div>";
                            $("#msgArea").append(str);
                        }
                    }
                    $('#msgDiv').scrollTop($('#msgDiv')[0].scrollHeight)
                    websocketOpen(roomId);
                }else{

                    roomId = null;
                    $("#msgArea").empty();
                    $("#chatRoomTitle").text(row.data().name);
                    $("#messengerDiv").css("display","none");
                    $("#chatRoomDiv").css("display","block");
                }
            },
            error(e) {
                console.log("error : " + e);
            },
                complete: function (){
                    messengerReadCount();
                }
        });
    });
}


/**
 * 채팅방 목록
 */
function roomList(){
    $.ajax({
        url: "/roomList",
        data: {"id" : user_info.id},
        type: "GET",
        success: function (data) {
            if (data != null) {
                roomList_Info = data;
                roomList_Init(roomList_Info);
            }
        },
        error(e) {
            console.log("error : " + e);
        }
    });
}


/**
 * 채팅방 목록 
 * @param data
 */
function roomList_Init(data){
    if ($.fn.DataTable.isDataTable('#roomList_dataTable')) {
        $('#roomList_dataTable').DataTable().destroy(); // 기존 인스턴스 제거
    }

    roomList_dataTable = $("#roomList_dataTable").DataTable({
        data: data,                                                     // 가져온 데이터
        "paging": false,                                                 // 페이징
        "searching": true,                                              // 검색 기능
        "info": false,                                                   // 정보 표시
        "ordering": false,                                               // 정렬 기능
        "scrollCollapse": true,
        "scrollY": '600px',
        "scrollX": "100%",
        "scrollXInner": "100px",
        "bAutoWidth": false,
        "language": {                                                   // 언어 설정
            decimal: "",
            emptyTable: "대화중인 방이 없습니다.",
            info: "Showing _START_ to _END_ of _TOTAL_ entries",
            infoEmpty: "Showing 0 to 0 of 0 entries",
            infoFiltered: "(filtered from _MAX_ total entries)",
            infoPostFix: "",
            thousands: ",",
            lengthMenu: " _MENU_ ",
            loadingRecords: "Loading...",
            processing: "Processing...",
            search: "Search",
            zeroRecords: "항목이 존재하지 않습니다",
            paginate: {
                "first": "처음",
                "last": "마지막",
                "next": "다음",
                "previous": "이전"
            },
            aria: {
                sortAscending: ": activate to sort column ascending",
                sortDescending: ": activate to sort column descending"
            }
        },
        "columnDefs": [
            { width: "95%", targets : [0] }
        ],
        "columns": [
            {
                data: null,
                render: function (data, type, row) {
                    if(row.profile == null || row.profile == ''){
                        let str ="";
                        if(data.unreadCount == 0){
                            str = "<div clas='row' style='width:100%'><div class='col-4' style='float: left'>";
                            str += "<img src='/uploadPath/image/topFile_cc49d4f3-5119-43b0-931e-5e7bb5aaa8df.jpg' id=' " + data.roomId + "'/>" + data.roomTitle + "</div>";
                            str += "<div class='col-7' style='float: left'>"+ data.roomDetail + "</div></div>";
                        }else{
                            str = "<div clas='row' style='width:100%'><div class='col-4' style='float: left'>";
                            str += "<img src='/uploadPath/image/topFile_cc49d4f3-5119-43b0-931e-5e7bb5aaa8df.jpg' id=' " + data.roomId + "'/>" + data.roomTitle + "</div>";
                            str += "<div class='col-7' style='float: left'>"+ data.roomDetail + "</div>";
                            str += "<div class='col-1' style='float: right'>"+ "<b style='color:red'>" + data.unreadCount + "</b>"+"</div></div>";
                        }

                        return str;

                    }else{
                        let str ="";
                        if(data.unreadCount == 0){
                            str = "<div clas='row' style='width:100%'><div class='col-4' style='float: left'>";
                            str += "<img src='/uploadPath/image/topFile_" + row.profile + "'" + "id=' " + data.roomId + "'/>" + data.roomTitle + "</div>";
                            str += "<div class='col-7' style='float: left'>"+ data.roomDetail + "</div></div>";
                        }else{
                            str = "<div clas='row' style='width:100%'><div class='col-4' style='float: left'>";
                            str += "<img src='/uploadPath/image/topFile_" + row.profile + "'" + "id=' " + data.roomId + "'/>" + data.roomTitle + "</div>";
                            str += "<div class='col-7' style='float: left'>"+ data.roomDetail + "</div>";
                            str += "<div class='col-1' style='float: right'>"+"<b style='color:red'>" + data.unreadCount + "</b>"+"</div></div>";
                        }

                        return str;

                    }
                }
            }
        ]
    });

    /* 선택된 tr 행 색상 변경 및 권한에 해당하는 메뉴 정보 가져오기*/
    $('#roomList_dataTable tbody').on('click', 'tr', function () {
        let tr = $(this).closest('tr');
        let row = roomList_dataTable.row(tr);

        if (tr.hasClass('selected')) {
            tr.removeClass('selected');

        } else {
            roomList_dataTable.$('tr.selected').removeClass('selected');//테이블에서 선택된 것 삭제
            tr.addClass('selected');

        }
    });

    // 리스너를 제거하고 다시 등록하는 과정으로 off 와 on 을 두어서 dbclick 할때 tr 행이 중복으로 눌리는 것을 방지한다
    $('#roomList_dataTable tbody').off('dblclick', 'tr').on('dblclick', 'tr', function () {
        let tr = $(this).closest('tr');
        let row = roomList_dataTable.row(tr);

        $.ajax({
            url: "/chatRoomEnter",
            type: "GET",
            data: {"id" : row.data().roomId},
            success: function (data) {
                if (data != null) {

                    // append 태그 삭제
                    $("#msgArea").empty();

                    // 채티방 타이틀 변경
                    $("#chatRoomTitle").text(row.data().roomTitle);
                    // 유저 목록 display none
                    $("#messengerDiv").css("display","none");
                    // 채팅방 display block
                    $("#chatRoomDiv").css("display","block");
                    // 채팅방 목록 display none
                    $("#roomDiv").css("display","none");

                    // 채팅방 id 
                    roomId = row.data().roomId;
                    for (let i = 0; i < data.length; i++) {
                        if(data[i].sendUserId == user_info.id){
                            let str = "<div clas='col-12' style='width:100%'><div class='col-10' style='float: right'>";
                            str += "<div style='text-align: right; background-color: #f8f9fc;'>";
                            str += "<b>" + data[i].roomDetail + "</b>";
                            str += "</div></div></div>";
                            $("#msgArea").append(str);
                        }else{
                            let str = "<div class='col-12' style='width:100%'><div class='col-10' style='float: left'>";
                            str += "<div style='text-align: left'>";
                            str += "<b>" + data[i].roomDetail + "</b>";
                            str += "</div></div></div>";
                            $("#msgArea").append(str);
                        }
                    }

                    // 스크롤 제일 하단으로 이동
                    $('#msgDiv').scrollTop($('#msgDiv')[0].scrollHeight)

                    // 채팅방 웹소켓 연결
                    websocketOpen(roomId);
                    //
                }
            },
            error(e) {
                console.log("error : " + e);
            },
            complete: function (){
                // 메세지를 읽음 처리
                messengerReadCount();
            }
        });
    });
}


// 이전 버튼 - 유저 목록으로 이동
function backChatRoom(){
    roomId = null;
    $("#messengerDiv").css("display","block");
    $("#chatRoomDiv").css("display","none");
    $("#roomDiv").css("display","none");
    $("#chatRoomTitle").text("");
    messenger();
}


// 메시지 전송
function sendMsg(){
    // 입력한 메시지 변수
    let sendMsg = $("#msg").val();

    // 입력한 데이터가 없다면 return
    if(sendMsg == ''){
        return;
    }
    // user_info.id 는 top.js 에서 계속 가지고 다니는 현재 사용자의 id
    $.ajax({
        url: "/sendMsg",
        type: "POST",
        data: {"id" : roomId , "roomDetail" : sendMsg, "targetId" : targetId , "userId" : user_info.id},
        success: function (data) {
            if (data.msg == "success") {
                // 채팅방 id
                roomId = data.roomId;
                // 메시지를 받는 상대방 id
                ws_targetUser = data.userId;
                
                // 웹소켓이 연결되어있지않으면 연결
                if(ws == null){
                    websocketOpen(roomId);
                }

                let sendData = new Object();
                sendData.roomId = roomId;
                sendData.roomDetail = sendMsg;
                // user_info.id 는 top.js 에서 계속 가지고 다니는 현재 사용자의 id
                sendData.sendUserId = user_info.id;

                // 채팅방 웹소켓에 전송
                ws.send(JSON.stringify(sendData));

                $("#msg").val("");
            }else {
                alert("서버에 오류가 발생하였습니다.");
            }
        },
        error(e) {
            console.log("error : " + e);
        },
        complete: function (){
            // widget 웹소켓이 연결되어있지않으면 연결
            if(ws_widget == null){
                // user_info.id 는 top.js 에서 계속 가지고 다니는 현재 사용자의 id
                widgetWebsocket(user_info.id);
            }
            let widgetData = new Object();
            widgetData.userId = ws_targetUser;
            // 메시지를 받는 상대방 id를 넣고 메시지 전달 -> 채팅방이 아닌 widget 용임
            ws_widget.send(JSON.stringify(widgetData));
            ws_targetUser = null;
        }
    });
}



// 이전 버튼 - 채팅방 목록으로 이동
function openRoomList(){
    $("#messengerDiv").css("display","none");
    $("#chatRoomDiv").css("display","none");
    $("#roomDiv").css("display","block");
    $("#chatRoomTitle").text("");
    if(ws != null){
        ws.close();
    }
    roomList();
}



/**
 * 웹소켓 연결
 * @param roomId
 */
function websocketOpen(roomId){
    // 본인의 ip와 port 입력
    ws = new WebSocket("ws://xxx.xxx.xxx.xxx:xxxx/ws/chat/"+roomId);

    // 웹소켓 연결 시 동작
    ws.onopen = function (data) {

    }
    // 웹소켓으로 메시지가 왔을때 동작
    ws.onmessage = function (data) {
        let messageJson = new Object();
        // 받은 데이터를 JSON 으로 변경
        messageJson = JSON.parse(data.data)
        if(messageJson.roomDetail != null || messageJson.roomDetail != ''){

            let msg = messageJson.roomDetail;

            if(messageJson.sendUserId == user_info.id){
                if (msg != null && msg.trim() != '') {
                    let str = "<div clas='col-12' style='width:100%'><div class='col-10' style='float: right'>";
                    str += "<div style='text-align: right; background-color: #f8f9fc;'>";
                    str += "<b>" + msg + "</b>";
                    str += "</div></div></div>";
                    $("#msgArea").append(str);
                }
            }else{
                if (msg != null && msg.trim() != '') {
                    let str = "<div class='col-12' style='width:100%'><div class='col-10' style='float: left'>";
                    str += "<div style='text-align: left'>";
                    str += "<b>" + msg + "</b>";
                    str += "</div></div></div>";
                    $("#msgArea").append(str);
                }
            }
            $('#msgDiv').scrollTop($('#msgDiv')[0].scrollHeight)
        }

    }
}


/**
 * widget 웹소켓 연결
 * @param username
 */
function widgetWebsocket(username){
    ws_widget = new WebSocket("ws://xxx.xxx.xxx.xxx:xxxx/ws/widget/"+username);
    
    ws_widget.onopen = function (data) {

    }
    ws_widget.onmessage = function (data) {
        let messageJson = new Object();
        messageJson = JSON.parse(data.data)

        console.log("messageJson : " + messageJson);

        // 사내 메시지 및 top 부분 메시지 알림
        if(messageJson.userId == user_info.id){
            messengerReadCount();
        }

        // 메신저 채팅방 목록이 열려 있는 경우
        if($("#roomDiv").css("display") == 'block'){
            roomList();
        }
    }
}

// 메시지 전송 버튼 enter 이벤트
$("#msg").on("keyup", function (event){
    if (event.key === 'Enter' || event.keyCode === 13) {
        sendMsg();
    }
})


$(document).ready(function () {
    // 읽지 않은 메시지 수 확인
    messengerReadCount();
    // widget 웹소켓 연결
    widgetWebsocket(user_info.id);

});

 

길어서 나중에 찾아볼때 애먹을 거 같다...

댓글