국비/Project-2 쇼핑몰

2023.06.16 97일차 Team Project 웹소켓 실시간 채팅

춘핑이 2023. 6. 26. 10:07

97일차 웹소켓 실시간 채팅

사용자 페이지 나누기

1. 서버 config

구현체를 등록했던 url정보를 방번호에 따라 구분할 수 있도록 변경해준다.

@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final SocketHandler socketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
        .addHandler(socketHandler, "/chat/{roomNumber}");
    }
}

url에서 chat이후 들어오는 roomNumber값은 앞으로 방을 구분하는 값이 될 것이다.

2. SocketHandler

@Slf4j
@Component
public class SocketHandler extends TextWebSocketHandler {
    // 웹소켓 세션을 담아둘 맵
    // HashMap<String, WebSocketSession> sessionMap = new HashMap<>();
    private List<HashMap<String, Object>> roomList = new ArrayList<>(); // 웹소켓 세션을 담아둘 리스트 ---roomListSessions

    private Gson gson = new Gson();

    // 웹소켓 연결이 되면 동작
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 소켓 연결시 session의 id를 키로 세션저장
        super.afterConnectionEstablished(session);

        // url에서 roomNumber를 추출해서 roomList에담을것임.
        boolean flag = false;
        String url = session.getUri().toString();
        String roomNumber = url.split("/chat/")[1];
        int idx = roomList.size(); // 방의 사이즈를 조사한다.

        if (roomList.size() > 0) {
            for (int i = 0; i < roomList.size(); i++) {
                String roomNum = (String) roomList.get(i).get("roomNumber");
                if (roomNum.equals(roomNumber)) {
                    flag = true;
                    idx = i;
                    break;
                }
            }
        }

        if (flag) { // 존재하는 방이라면 세션만 추가한다.
            HashMap<String, Object> map = roomList.get(idx);
            map.put(session.getId(), session);
        } else { // 최초 생성하는 방이라면 방번호와 세션을 추가한다.
            HashMap<String, Object> map = new HashMap<>();
            map.put("roomNumber", roomNumber);
            map.put(session.getId(), session);
            roomList.add(map);
        }

        // 세션 등록이 끝나면 발급받은 세션 ID값의 메시지를 발송한다.
        Map<String, String> messageData = new HashMap<>();
        messageData.put("type", "getId");
        messageData.put("sessionId", session.getId());
        String json = gson.toJson(messageData);
        session.sendMessage(new TextMessage(json));
    }

    // 메시지를 수신하면 실행
    // 상속받은 TextWebSocketHandler는 handleTextMessage를 실행시키고
    // 메시지 타입에따라 handleBinaryMessage또는 handleTextMessage가 실행된다.
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // 메시지 발송
        String msg = message.getPayload();
        JsonObject obj = jsonToObjectParser(msg);
        String roomNum = obj.get("roomNumber").getAsString();

        HashMap<String, Object> temp = new HashMap<>();
        if (roomList.size() > 0) {
            for (int i = 0; i < roomList.size(); i++) {
                String roomNumber = (String) roomList.get(i).get("roomNumber"); // 세션리스트의 저장된 방번호를 가져와서
                if (roomNumber.equals(roomNum)) { // 같은값의 방이 존재한다면
                    temp = roomList.get(i); // 해당 방번호의 세션리스트의 존재하는 모든 object값을 가져온다.
                    break;
                }
            }

            // 해당 방의 세션들만 찾아서 메시지를 발송해준다.
            for (String k : temp.keySet()) {
                if (k.equals("roomNumber")) { // 다만 방번호일 경우에는 건너뛴다.
                    continue;
                }

                WebSocketSession wss = (WebSocketSession) temp.get(k);
                if (wss != null) {
                    try {
                        String json = gson.toJson(obj);
                        wss.sendMessage(new TextMessage(json));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        session.close();
        // 소켓 종료
        if (roomList.size() > 0) { // 소켓이 종료되면 해당 세션값들을 찾아서 지운다.
            for (int i = 0; i < roomList.size(); i++) {
                roomList.get(i).remove(session.getId());
            }
        }
        super.afterConnectionClosed(session, status);
    }

    private static JsonObject jsonToObjectParser(String jsonStr) {
        JsonObject obj = null;
        try {
            obj = JsonParser
                    .parseString(jsonStr)
                    .getAsJsonObject();
        } catch (JsonParseException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

구현체의 handler이다.
1.세션을 관리하던 map객체에서 list, hashmap형태로 변경되었다. hashmap의 value자료형도 WebSocketSession에서 Object형으로 변경되었습니다.
2.세션을 저장할때, 현재 접근한 방의 정보가 있는지 체크하고 존재하지 않으면 방의 번호를 입력 후 세션들을 담아주는 로직으로 변경되었다.
3.마찬가지로 종료시에도 list컬랙션을 순회하면서 해당 키값의 세션들을 삭제하도록 변경되었다.
4.메시지를 발송하는 handleTextMessage메소드에서는 현재의 방번호를 가져오고 방정보+세션정보를 관리하는 rls리스트 컬랙션에서 데이터를 조회한 후에 해당 Hashmap을 임시 맵에 파싱하여 roomNumber의 키값을 제외한 모든 세션키값들을 웹소켓을 통해 메시지를 보내준다.

3. 클라이언트단

let customerId = $('#customerId').val();
    let roomNumber = $("#roomNumber").val();

    //웹소켓 객체
    let ws;

    function wsOpen() {
        try {
            ws = new WebSocket("ws://" + location.host + "/chat/" + roomNumber);

            const connectionTime = 3 * 60 * 1000;

            if (ws !== null) {
                // 일정 시간이 지난 후에 연결을 닫음
                setTimeout(function() {
                    if (ws.readyState === WebSocket.OPEN) {
                        disconnect();
                    }
                }, connectionTime);
            }

            wsEvt();
        } catch (error) {
            // 웹소켓 연결 실패 처리
            $("#messageTextArea").append(`<p>웹소켓 연결에 오류가 발생했습니다...</p>`);
        }
    }

    function wsEvt() {
        //소켓이 열리면 초기화 세팅하기
        ws.onopen = function(data) {
            console.log("웹소켓연결됨!!");
            $("#messageTextArea").append(`<p>서버에 연결되었습니다... </br>
        채팅에 응답이 없을시 1대1문의를 이용해주세요...!</p>`);
        }

        ws.onerror = function(event) {
            $("#messageTextArea").append(`<p>오류가 발생했습니다...</p>`);
        };

        ws.onmessage = function(data) {
            let msg = data.data;
            if (msg != null && msg.trim() != '') {
                let serverJson = JSON.parse(msg);
                if (serverJson.type === "getId") {
                    let sId = serverJson.sessionId != null ? serverJson.sessionId : "";
                    if (sId != '') {
                        $("#sessionId").val(sId);
                    }
                } else if (serverJson.type === "message") {
                    if (serverJson.sessionId === $("#sessionId").val()) {
                        $("#messageTextArea").append("<div class='me'>" + customerId + "님: " + serverJson.msg + "</div>");
                    } else {
                        $("#messageTextArea").append("<div class='admin'>관리자: " + serverJson.msg + "</div>");
                    }
                } else {
                    console.warn("unknown type!")
                }
            }
        }

        ws.onclose = function(event) {
            $("#messageTextArea").append(`<p>채팅 연결이 해제 되었습니다.</p>`);
            console.log("연결해제됨!!!");
        };

        // enter눌리면 send() 실행
        $(document).keypress(function(e) {
            if (e.which == 13) {
                e.preventDefault(); // 엔터 키의 기본 동작 방지
                send();
            }
        });

        $("#sendMessageBtn").click(function() {
            send();
        });
    }

    function send() {
        let data = {
            type: "message",
            roomNumber: roomNumber,
            sessionId: $("#sessionId").val(),
            userName: customerId,
            msg: $("#textMessage").val()
        }
        ws.send(JSON.stringify(data));
        $('#textMessage').val("");
    }

    function disconnect() {
        if (ws !== null) {
            let data = {
                type: "close",
                roomNumber: roomNumber,
                sessionId: $("#sessionId").val(),
                userName: customerId,
                msg: $("#textMessage").val()
            }
            //닫았다는 메시지 보내기
            ws.send(JSON.stringify(data));

            //닫기
            ws.close();
            ws = null;

            $.ajax("/chat/deleteRoom", {
                method: "post",
                data: {
                    roomNumber: roomNumber
                }
            });
        }
    }
    //초기화
    wsOpen();

<div id="chatmodal" class="modal">
    <div class="modal-content">
        <div class="chatcontainer">
            <div id="roomContainer" class="roomContainer">
                <table id="roomList" class="roomList mx-auto"></table>
            </div>
        </div>
        <iframe id="customerFrame" frameborder="0"></iframe>
        <button class="close btn btn-danger">닫기</button>
    </div>
</div>

모달 창을 통해서 chatroom이 나오게 설정했다.
1.방의 번호값을 모델에서 저장한 값을 jstl을 통해 파싱한다.
2.접속한 방의 이름을 모델에서 저장한 값을 가져와서 채팅방 이름을 추가해준다. ${roomName}
3.메시지를 보내는 send함수에 roomNumber 키 값이 추가되었다.
방의 번호를 보내줌으로써 소켓서버는 어느방에서 메시지를 보냈는지 구분한다.

2023.08.02

이다음 부터는 블로그에 없이 직접 커스텀해나가는 것이다.
이전 커밋기록이라 정확하게 작성하기는 어렵지만 이어서 작성해보도록 하겠다.