스프링

스프링부트 웹소켓 실시간 푸시 알림(좋아요, 댓글)

nan2 2022. 6. 20. 11:58
반응형

1. 스프링부트 웹 소켓 라이브러리 추가

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket'

 

2. EchoHandler 작성

package com.reviewer.portfolio.controller;

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

import org.springframework.stereotype.Component;
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 org.thymeleaf.util.StringUtils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@RequiredArgsConstructor
public class EchoHandler extends TextWebSocketHandler{
	
	// 전체 로그인 유저
	private List<WebSocketSession> sessions = new ArrayList<>();
	
	// 1대1 매핑
	private Map<String, WebSocketSession> userSessionMap = new HashMap<>();
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		log.info("Socket 연결");
		sessions.add(session);
		log.info(sendPushUsername(session));				//현재 접속한 사람의 username이 출력됨
		String senderId = sendPushUsername(session);
		userSessionMap.put(senderId, session);
	}

	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		log.info("session = " + sendPushUsername(session));
		String msg = message.getPayload();				//js에서 넘어온 메세지
		log.info("msg = " + msg);
		
		if (!StringUtils.isEmpty(msg)) {
			String[] strs = msg.split(",");
			
			if (strs != null && strs.length == 5) {
				String pushCategory = strs[0];			//댓글, 좋아요 구분
				String replyWriter = strs[1];			//댓글, 좋아요 보낸 유저
				String sendedPushUser = strs[2];		//푸시 알림 받을 유저
				String boardId = strs[3];				//게시글번호
				String title = strs[4];					//게시글제목
				
				WebSocketSession sendedPushSession = userSessionMap.get(sendedPushUser);	//로그인상태일때 알람 보냄
				
				//부모댓글
				if ("reply".equals(pushCategory) && sendedPushSession != null) {
					TextMessage textMsg = new TextMessage(replyWriter + " 님이 " + "<a href='/porfolDetail/" + boardId + "' style=\"color:black\"><strong>" + title + "</strong> 에 댓글을 남겼습니다.</a>");
					sendedPushSession.sendMessage(textMsg);
				}
				
				//좋아요
				else if ("like".equals(pushCategory) && sendedPushSession != null) {
					TextMessage textMsg = new TextMessage(replyWriter + " 님이 " + "<a href='/porfolDetail/" + boardId + "' style=\"color:black\"><strong>" + title + "</strong> 을 좋아요♡ 했습니다.</a>");
					sendedPushSession.sendMessage(textMsg);
				}
				
				//자식댓글
				else if ("reReply".equals(pushCategory) && sendedPushSession != null) {
					TextMessage textMsg = new TextMessage(replyWriter + " 님이 " + "<a href='/porfolDetail/" + boardId + "' style=\"color:black\"><strong>" + title + "</strong> 글의 회원님 댓글에 답글을 남겼습니다.</a>");
					sendedPushSession.sendMessage(textMsg);
				}
			}
		}
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		log.info("Socket 연결 해제");
		sessions.remove(session);
		userSessionMap.remove(sendPushUsername(session), session);
	}

	//알람을 보내는 유저(댓글작성, 좋아요 누르는 유저)
	private String sendPushUsername(WebSocketSession session) {
		String loginUsername;
		
		if (session.getPrincipal() == null) {
			loginUsername = null;
		} else {
			loginUsername = session.getPrincipal().getName();
		}
		return loginUsername;
	}
}

 

sessions에는 로그인 상태인 전체 유저의 정보가 담기고, userSessionMap에는 1대 1로 유저사이에 알림을 보낼 때 유저 정보를 담는다.

 

[로그인을 스프링 시큐리티로 구현한 경우 - getPrincipal().getName() ]

session이 연결된 후 sendPushUsername() 함수를 이용하여 WebSocketSession 의 유저 정보를 getPrincipal().getName() 이용하여 user의 username을 알아낸다.

 

userSessionMap 에 key(username) : value(WebSocketSession)으로 넣어준다.

 

 

3. WebSocket Configuration 설정

package com.reviewer.portfolio.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import com.reviewer.portfolio.controller.EchoHandler;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSocket					//웹소켓 활성화
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer{
	
	private final EchoHandler echoHandler;

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    	//클라이언트에서 웹소켓에 접속하기위한 경로(ex: localhost:8080/push)
        //도메인이 다른 서버에서도 접속 가능하도록 CORS 설정 추가
        //소켓을 지원하지 않는 브라우저의 경우 SockJS 사용하도록 설정 추가
		registry.addHandler(echoHandler, "/push").setAllowedOriginPatterns("*").withSockJS();
	}
	
}

 

4. 뷰

//좋아요 알림이 보이는 부분
<div id="socketAlertDiv">
	<div id="socketAlert" class="alert alert-warning" role="alert"></div>
</div>

전체 페이지에서 모두 header.html와 footer.html을 가지고 있기 때문에 우선 알림을 header.html에 보여지도록 해줌

 

var socket = null;
$(document).ready(function(){
	//소켓 연결
	connectWs();
});

function connectWs(){
	//WebSocketConfig에서 설정한 endPoint("/push")로 연결
	var ws = new SockJS("/push");
	socket = ws;
	
	ws.onopen = function() {
		console.log('open');
	};

	ws.onmessage = function(event) {
		console.log(event.data);
		let $socketAlert = $('#socketAlert');
        //EchoHandler에서 설정한 메세지 넣어줌
		$socketAlert.html(event.data)
		$socketAlert.css('display', 'block');
		
        //일정 시간 지나면 알림 사라짐
		setTimeout(function(){
			$socketAlert.css('display','none');
		}, 5000);
	};

	ws.onclose = function() {
		console.log('close');
	};
 
};

 

/* 부모댓글 등록 */	
$('.replyAddBtn').on('click', function(){
	//웹 소켓의 알림 메시지에 필요한 정보들
	var boardId = $('#boardId').val();
	var boardTitle = $('#porfolTitle').text();
	var boardWriter = $('#boardWriter').val();
	var replyWriter = $('#replyWriter').val();
	var replyText = $('#replyText').val();
	var param = { "boardId" : boardId, "replyText" : replyText};

	$.ajax({
		url 	: "/reply",
		type 	: "post",
		data 	: param,
		success : function(resp){
			alert('댓글이 등록되었습니다.');
			location.reload();
			//웹 소켓 관련 로직 추가
			if (boardWriter != replyWriter){	//글쓴이와 댓글작성자가 다를 경우 소켓으로 메세지 보냄
				if (socket){
					let socketMsg = "reply," + replyWriter + "," + boardWriter + "," + boardId + "," + boardTitle;
					socket.send(socketMsg);
				}
			}
		},
		error : function(XMLHttpRequest, textStatus, errorThrown){
			alert('댓글 등록이 실패하였습니다.');
		}
	});
});

 

댓글 작성, 좋아요 할 경우 ajax 호출 성공 후 로직에서 /push로 알림과 메세지를 전달하면 EchoHandler 의 handleTextMessage() 함수가 동작한다.

 

 

반응형