STOMP로 채팅 만들기
2022. 2. 25. 02:17ㆍ스프링부트

구현 방식
메인화면에 접속하면 웹소켓을 연결하고 /roomList를 구독합니다
사용자가 방을 만들면 /roomList로 메세지를 보내 새로운 방이 만들어졌다는 걸 알립니다
메인화면에 있는 사용자들은 /roomList로 메세지를 받으면 방목록을 불러옵니다
방을 만들거나 다른사람이 만든 방에 들어갈 경우 /roomList의 구독을 취소하고 /message/방번호(UUID)를 구독합니다
방 안에서 같은 방번호를 구독한 사람끼리 메세지를 주고받을 수 있습니다
스프링부트와 jsp를 사용했고 아래는 사용 라이브러리입니다

여기에 jsp만 추가해서 사용했습니다
<!-- JSP 템플릿 엔진 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency>
application.properties에 viewresolver를 설정합니다
spring.mvc.view.prefix=/WEB-INF/view/ spring.mvc.view.suffix=.jsp
프로젝트 구조

웹소켓 설정
WebSocketConfig.java
@EnableWebSocketMessageBroker @Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/websocket").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); } }
Message.java
@Data public class Message { private String message; private String nickname; private Date date; Message(){ date = new Date(); } }
ChatingRoom.java
@Data @Builder public class ChatingRoom { private String roomNumber; private String roomName; private LinkedList<String> users; @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ChatingRoom other = (ChatingRoom) obj; return Objects.equals(roomNumber, other.roomNumber); } @Override public int hashCode() { return Objects.hash(roomNumber); } }
채팅방에는 방번호, 방이름, 유저목록이 있는데 채팅방 목록에서 방번호로 방을 찾을 수 있게 equals와 hashCode를 오버라이딩 했고 생성자를 사용하지 않고 Builder패턴을 사용했습니다
메세지처리를 할 컨트롤러와 각종 유틸메서드는 클래스를 추가해 따로 관리해야 하지만 분리하지 않고 MainContoller 한곳에 모아놨습니다
MainController.java
@Controller public class MainController { // 채팅방 목록 public static LinkedList<ChatingRoom> chatingRoomList = new LinkedList<>(); // ---------------------------------------------------- // 유틸 메서드 // 방 번호로 방 찾기 public ChatingRoom findRoom(String roomNumber) { ChatingRoom room = ChatingRoom.builder().roomNumber(roomNumber).build(); int index = chatingRoomList.indexOf(room); if(chatingRoomList.contains(room)) { return chatingRoomList.get(index); } return null; } // 쿠키에 추가 public void addCookie(String cookieName, String cookieValue) { ServletRequestAttributes attr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes(); HttpServletResponse response = attr.getResponse(); Cookie cookie = new Cookie(cookieName, cookieValue); int maxage = 60 * 60 * 24 * 7; cookie.setMaxAge(maxage); response.addCookie(cookie); } // 방 번호, 닉네임 쿠키 삭제 public void deleteCookie( ) { ServletRequestAttributes attr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes(); HttpServletResponse response = attr.getResponse(); Cookie roomCookie = new Cookie("roomNumber", null); Cookie nicknameCookie = new Cookie("nickname",null); roomCookie.setMaxAge(0); nicknameCookie.setMaxAge(0); response.addCookie(nicknameCookie); response.addCookie(roomCookie); } // 쿠키에서 방번호, 닉네임 찾기 public Map<String, String> findCookie() { ServletRequestAttributes attr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = attr.getRequest(); Cookie[] cookies = request.getCookies(); String roomNumber = ""; String nickname= ""; if(cookies == null) { return null; } if(cookies != null) { for(int i=0;i<cookies.length;i++) { if("roomNumber".equals(cookies[i].getName())) { roomNumber = cookies[i].getValue(); } if("nickname".equals(cookies[i].getName())) { nickname = cookies[i].getValue(); } } if(!"".equals(roomNumber) && !"".equals(nickname)) { Map<String, String> map = new HashMap<>(); map.put("nickname", nickname); map.put("roomNumber", roomNumber); return map; } } return null; } // 닉네임 생성 public void createNickname(String nickname) { addCookie("nickname", nickname); } // 방 입장하기 public boolean enterChatingRoom(ChatingRoom chatingRoom, String nickname) { createNickname(nickname); if(chatingRoom == null) { deleteCookie(); return false; } else { LinkedList<String> users = chatingRoom.getUsers(); users.add(nickname); addCookie("roomNumber", chatingRoom.getRoomNumber()); return true; } } // ---------------------------------------------------- // 컨트롤러 // 메인화면 @GetMapping("/") public String main() { return "main"; } // 채팅방 목록 @GetMapping("/chatingRoomList") public ResponseEntity<?> chatingRoomList() { return new ResponseEntity<LinkedList<ChatingRoom>>(chatingRoomList, HttpStatus.OK); } // 방 만들기 @PostMapping("/chatingRoom") public ResponseEntity<?> chatingRoom(String roomName, String nickname) { // 방을 만들고 채팅방목록에 추가 String roomNumber = UUID.randomUUID().toString(); ChatingRoom chatingRoom = ChatingRoom.builder() .roomNumber(roomNumber) .users(new LinkedList<>()) .roomName(roomName) .build(); chatingRoomList.add(chatingRoom); // 방 입장하기 enterChatingRoom(chatingRoom, nickname); return new ResponseEntity<>(chatingRoom, HttpStatus.OK); } // 방 들어가기 @GetMapping("/chatingRoom-enter") public ResponseEntity<?> EnterChatingRoom(String roomNumber, String nickname){ // 방 번호로 방 찾기 ChatingRoom chatingRoom = findRoom(roomNumber); if(chatingRoom == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { // 방 들어가기 enterChatingRoom(chatingRoom, nickname); return new ResponseEntity<>(chatingRoom, HttpStatus.OK); } } // 방 나가기 @PatchMapping("/chatingRoom-exit") public ResponseEntity<?> ExitChatingRoom(){ Map<String, String> map = findCookie(); if(map == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } String roomNumber = map.get("roomNumber"); String nickname = map.get("nickname"); // 방목록에서 방번호에 맞는 유저목록 가져오기 ChatingRoom chatingRoom = findRoom(roomNumber); List<String> users = chatingRoom.getUsers(); // 닉네임 삭제 users.remove(nickname); // 쿠키에서 닉네임과 방번호 삭제 deleteCookie(); // 유저가 한명도 없다면 방 삭제 if(users.size() == 0) { chatingRoomList.remove(chatingRoom); } return new ResponseEntity<>(chatingRoom, HttpStatus.OK); } // 참가 중이었던 대화방 @GetMapping("/chatingRoom") public ResponseEntity<?> chatingRoom() { // 쿠키에 닉네임과 방번호가 있다면 대화중이던 방이 있던것 Map<String, String> map = findCookie(); if(map == null) { return new ResponseEntity<>(HttpStatus.OK); } String roomNumber = map.get("roomNumber"); String nickname = map.get("nickname"); ChatingRoom chatingRoom = findRoom(roomNumber); if(chatingRoom == null) { deleteCookie(); return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { Map<String, Object> map2 = new HashMap<>(); map2.put("chatingRoom", chatingRoom); map2.put("myNickname", nickname); return new ResponseEntity<>(map2, HttpStatus.OK); } } // ---------------------------------------------------- // 메세지 컨트롤러 // 여기서 메세지가 오면 방목록 업데이트 @MessageMapping("/socket/roomList") @SendTo("/topic/roomList") public String roomList() { return ""; } // 채팅방에서 메세지 보내기 @MessageMapping("/socket/sendMessage/{roomNumber}") @SendTo("/topic/message/{roomNumber}") public Message sendMessage(@DestinationVariable String roomNumber, Message message) { return message; } // 채팅방에 입장 퇴장 메세지 보내기 @MessageMapping("/socket/notification/{roomNumber}") @SendTo("/topic/notification/{roomNumber}") public Map<String, Object> notification(@DestinationVariable String roomNumber, Map<String, Object> chatingRoom) { return chatingRoom; } }
main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>채팅</title> <link rel="stylesheet" href="/css/main.css"> </head> <body> <!-- 채팅방 목록 --> <main> <h1>채팅방</h1> <button class="new_chat">새 채팅방 만들기</button> <nav> <span>방 제목</span> <span>인원</span> </nav> <hr> <ul> <li> <!-- <span class="chat_title"></span> <span class="chat_count"></span> --> </li> </ul> </main> <!-- 채팅방 목록 --> <!-- 채팅방 입장 --> <div class="chat"> <div> <div class="chat_body"> <h2 class="chat_title">1번방</h2> <button class="chat_back">◀</button> <ul class="chat_list"> <li> <!-- <div class="notification"> <span></span> </div> --> </li> </ul> <div class="chat_input"> <div class="chat_input_area"> <textarea></textarea> </div> <div class="chat_button_area"> <button>전송</button> </div> </div> </div> <div class="chat_users"> <h2> 참가인원 <span class="user"></span> </h2> <div class="chat_nickname"> <ul> <li> </li> </ul> </div> </div> </div> </div> <!-- 채팅방 입장 --> <!-- sock js --> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.2/sockjs.min.js"></script> <!-- STOMP --> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script> <script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script> <script src="/js/main.js"></script> </body> </html>
main.css
@charset "UTF-8"; * { margin: 0; padding: 0; } li { list-style: none; } html { font-size: 62.5%; } .swal-footer { text-align: center; } /* 채팅방 목록 */ main { width: 35vw; min-width: 300px; height: 90vh; border: 1px solid #000; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } main h1 { font-size: 2rem; padding: 10px; height: 50px; box-sizing: border-box; } main .new_chat { position: absolute; top: 10px; right: 10px; border-radius: 3px; padding: 3px; background: #fff; cursor: pointer; } main .new_chat:hover { background: #eee; } main nav { font-size: 1.6rem; display: flex; text-align: center; margin-bottom: 5px; padding: 0 7px; } main nav > span:first-child { width: 80%; } main nav > span:last-child { width: 20%; } main ul { overflow-y: auto; max-height: calc(100% - 50px); } main li { font-size: 1.6rem; width: 100%; height: 70px; padding: 7px; box-sizing: border-box; display: flex; justify-content: space-between; } main li:hover { background: #eee; } main .chat_title { font-weight: bold; width: 80%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } main .chat_count { width: 20%; text-align: center; } /* 채팅방 목록 */ /* 채팅방 안 */ .chat { width: 50vw; min-width: 300px; height: 90vh; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 10; display: none; } .chat > div { height: 100%; display: flex; } .chat .chat_body { background: rgb(195, 204, 221); height: 100%; width: 70%; order: 1; border: 1px solid #000; position: relative; } .chat h2 { font-size: 2rem; padding: 10px; height: 50px; box-sizing: border-box; text-align: center; background: #eee; } .chat .chat_back { font-size: 2rem; width: 50px; height: 50px; position: absolute; top: 0; left: 0; border: none; cursor: pointer; } .chat ul.chat_list { overflow-y: auto; height: calc(100% - 150px); box-sizing: border-box; padding: 5px 0; } .chat li { font-size: 1.5rem; } .chat li > div { padding: 10px; display: flex; } .chat li .notification { text-align: center; } .chat li .notification span { margin: 0 auto; border-radius: 15px; padding: 5px 10px; background: #ae9191; color: #fff; } .chat li > div .nickname { padding: 3px; } .chat li > div .message { display: flex; } .chat .chat_me { justify-content: end; } .chat .chat_other { justify-content: start; } .chat .chat_other .chat_in_time { order: 1; } .chat .chat_in_time { font-size: 1.3rem; margin: 0 5px; display: flex; align-items: flex-end; } .chat .chat_content { padding: 5px; border-radius: 3px; box-shadow: 0px 2px 3px 0px rgb(0 0 0 / 25%); display: inline-block; max-width: 250px; max-height: 400px; overflow-y: auto; word-break: break-all; } .chat .chat_me .chat_content { background: yellow; } .chat .chat_other .chat_content { background: #fff; } .chat .chat_input { height: 100px; display: flex; background: #fff; } .chat_input .chat_input_area { width: 87%; } .chat_input .chat_input_area textarea { width: 100%; height: 100%; border: none; resize: none; padding: 8px; box-sizing: border-box; } .chat_input .chat_input_area textarea:focus { outline: none; } .chat_input .chat_button_area { width: 13%; } .chat_input .chat_button_area button { background: yellow; border: 1px solid #ddd; border-radius: 3px; padding: 5px; width: 90%; margin-top: 10px; cursor: pointer; } .chat .chat_users { border: 1px solid #ddd; width: 30%; height: 300px; /* position: absolute; right: 101%; top: 0; */ margin-right: 3px; } /* 참가인원 */ .chat .chat_users h2 { font-size: 1.6rem; } .chat .chat_users .chat_nickname { font-size: 1.6rem; height: calc(100% - 50px); overflow: auto; } .chat .chat_users .chat_nickname li { padding: 5px; } /* 참가인원 */ /* 채팅방 안 */ @media(max-width: 1024px) { html { font-size: 60%; } main { width: 99vw; } .chat { width: 99vw; } }
main.js
$(document).ready(function(){ // 방 목록 그리기 const listHtml = function(roomList) { let listHtml = ""; for(let i=roomList.length-1;i>=0;i--) { listHtml += ` <li data-room_number=${roomList[i].roomNumber}> <span class="chat_title">${roomList[i].roomName }</span> <span class="chat_count">${roomList[i].users.length}명</span> </li>`; } $("main ul").html(listHtml); } // 채팅방 목록 불러오기 const chatingRoomList = function(){ $.ajax({ url: "/chatingRoomList", type: "GET", }) .then(function(result){ listHtml(result) }) .fail(function(){ alert("에러가 발생했습니다"); }) } const socket = new SockJS('/websocket'); const stomp = Stomp.over(socket); stomp.debug = null; // stomp 콘솔출력 X // 구독을 취소하기위해 구독 시 아이디 저장 const subscribe = []; // 모든 구독 취소하기 const subscribeCancle = function() { const length = subscribe.length; for(let i=0;i<length;i++) { const sid = subscribe.pop(); stomp.unsubscribe(sid.id); } } // 메인 화면 const main = function() { $("main").show(); // 기존 구독 취소 subscribeCancle(); // 채팅 중이었던 방이 있을때 const room = chatingRoom(); if(room) { return; } const subscribeId = stomp.subscribe("/topic/roomList", function(){ // "/topic/roomList"에서 메세지가 왔을때 실행할 함수 chatingRoomList(); }); subscribe.push(subscribeId); chatingRoomList(); }; stomp.connect({}, function(){ main(); }); // ----------------- 메인화면 --------------------------- // ----------------- 채팅방 --------------------------- const info = (function(){ let nickname = ""; let roomNumber = ""; const getNickname = function() { return nickname; } const setNickname = function(set){ nickname = set; } const getRoomNumber = function() { return roomNumber; } const setRoomNumber = function(set) { roomNumber = set; } return { getNickname : getNickname, setNickname : setNickname, getRoomNumber : getRoomNumber, setRoomNumber : setRoomNumber, } })(); const errorMSG = function(result){ if(result.status == 404) { alert("종료되었거나 없는 방입니다"); } else { alert("에러가 발생했습니다"); } location.href = "/"; } // 참가자 그리기 const userList = function(users){ $(".chat .chat_users .user").text(users.length + "명"); let userHtml = ""; for(let i=0;i<users.length;i++) { userHtml += ` <li>${users[i] }</li>`; } $(".chat .chat_nickname ul").html(userHtml); } // 메세지 그리기 const chating = function(messageInfo){ let nickname = messageInfo.nickname; let message = messageInfo.message; message = message.replaceAll("\n", "<br>").replaceAll(" ", " "); const date = messageInfo.date; const d = new Date(date); const time = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0"); let sender = ""; if(info.getNickname() == nickname) { sender = "chat_me"; nickname = ""; } else { sender= "chat_other"; } const chatHtml = ` <li> <div class=${sender }> <div> <div class="nickname">${nickname }</div> <div class="message"> <span class=chat_in_time>${time }</span> <span class="chat_content">${message }</span> <span> </div> </div> </li>`; $(".chat ul.chat_list").append(chatHtml); $(".chat ul").scrollTop($(".chat ul")[0].scrollHeight); } // 채팅방 구독 const chatingConnect = function(roomNumber){ // 기존 구독 취소 subscribeCancle(); // 메세지를 받을 경로 const id1 = stomp.subscribe("/topic/message/" + roomNumber, function(result){ const message = JSON.parse(result.body); // 메세지가 왔을때 실행할 함수 chating(message); }) // 입장,퇴장 알림을 받을 경로 const id2 = stomp.subscribe("/topic/notification/" + roomNumber, function(result){ const room = JSON.parse(result.body); const message = room.message; // 메세지가 왔을때 실행할 함수 userList(room.users); const chatHtml = ` <li> <div class="notification"> <span>${message}</span> </div> </li>`; $(".chat ul.chat_list").append(chatHtml); $(".chat ul").scrollTop($(".chat ul")[0].scrollHeight); }) subscribe.push(id1); subscribe.push(id2); } // 채팅방 세팅 const initRoom = function(room, nickname) { // 방 목록 업데이트 stomp.send("/socket/roomList"); $("main").hide(); info.setNickname(nickname); info.setRoomNumber(room.roomNumber); $(".chat").show(); $(".chat .chat_title").text(room.roomName); userList(room.users); chatingConnect(room.roomNumber); $(".chat_input_area textarea").focus(); } // 메세지 보내기 const sendMessage = function(){ const message = $(".chat_input_area textarea"); if (message.val() == "") { return; } const roomNumber = info.getRoomNumber(); const nickname = info.getNickname(); const data = { message : message.val(), nickname : nickname, } stomp.send("/socket/sendMessage/" + roomNumber, {}, JSON.stringify(data)); message.val(""); } $(".chat_button_area button").click(function() { sendMessage(); $(".chat_input_area textarea").focus(); }) $(".chat_input_area textarea").keypress(function(event) { if (event.keyCode == 13) { if (!event.shiftKey) { event.preventDefault(); sendMessage(); } } }) // 닉네임 만들고 채팅방 들어가기 const enterChatingRoom = function(roomNumber) { swal({ text: "사용하실 닉네임을 입력해주세요", content: "input", buttons: ["취소", "확인"], closeOnClickOutside : false }) .then(function(nickname){ if(nickname) { const data = { roomNumber : roomNumber, nickname : nickname } $.ajax({ url: "/chatingRoom-enter", type: "GET", data: data, }) .then(function(room){ initRoom(room, nickname); // 채팅방 참가 메세지 room.message = nickname + "님이 참가하셨습니다"; stomp.send( "/socket/notification/" + roomNumber, {}, JSON.stringify(room)); }) .fail(function(result){ errorMSG(result); }) } }) } // 새 채팅방 만들기 const createRoom = function(roomName) { swal({ text: "사용하실 닉네임을 입력해주세요", content: "input", buttons: ["취소", "확인"], closeOnClickOutside : false }) .then(function(nickname){ if(nickname) { const data = { roomName : roomName, nickname : nickname } $.ajax({ url: "/chatingRoom", type: "POST", data: data, }) .then(function(room){ initRoom(room, nickname) }) .fail(function(){ alert("에러가 발생했습니다"); }) } }) } $(".new_chat").click(function(){ swal({ text: "방 이름을 입력해주세요", content: "input", buttons: ["취소", "확인"], closeOnClickOutside : false }) .then(function(roomName){ if(roomName) { createRoom(roomName); } }) }) $(document).on("dblclick", "main li", function(){ const roomNumber = $(this).data("room_number"); enterChatingRoom(roomNumber); }) // 채팅방 나가기 $(".chat_back").click(function() { swal({ text: "대화방에서 나갈까요?", buttons: ["취소", "확인"] }) .then(function(result){ if(result) { $.ajax({ url: "/chatingRoom-exit", type: "PATCH", }) .then(function(room){ const roomNumber = info.getRoomNumber(); if(room.users.length != 0) { // 채팅방 나가기 메세지 room.message = info.getNickname() + "님이 퇴장하셨습니다"; stomp.send( "/socket/notification/" + roomNumber, {}, JSON.stringify(room)); } // 채팅방 목록 업데이트 stomp.send("/socket/roomList"); main(); $(".chat").hide(); $(".chat ul.chat_list").html(""); info.setRoomNumber(""); info.setNickname(""); }) .fail(function(){ errorMSG(); }) } }) }) // 대화 중이던 방 const chatingRoom = function (){ let returnRoom = null; $.ajax({ url: "/chatingRoom", type: "GET", async: false, }) .then(function(result){ if(result != "") { const room = result.chatingRoom; const nickname = result.myNickname; initRoom(room, nickname); returnRoom = result; } }) .fail(function(result){ errorMSG(result); }) return returnRoom; }; }) // document.ready
'스프링부트' 카테고리의 다른 글
| 스프링부트+jsp로 배달사이트 만들기-41 관리자가 주문취소시 환불하기 추가 (1) | 2022.01.20 |
|---|---|
| 스프링부트+jsp로 배달사이트 만들기-40 결제api 사용해서 주문하기(아임포트) (3) | 2022.01.09 |
| sts4 깃 임포트 후 db 연결 에러 (0) | 2022.01.07 |
| 스프링부트+jsp로 배달사이트 만들기-39 비밀번호 찾기, 비밀번호 변경 (0) | 2022.01.04 |
| 스프링부트+jsp로 배달사이트 만들기-38 이메일보내기(아이디 찾기,비번찾기) (0) | 2021.12.29 |