해당 프로젝트는 2023/01/25 ~ 2023/03/12 내에 진행되는
아카데미 내 수강생들끼리 팀을 나누어 진행한 모의 프로젝트입니다.
팀원은 5명이었으며, 프로젝트 리더를 맡았습니다.
이전 글 목록
1) 주어진 RFP를 바탕으로 주제 선정 - Spring Project(OTT 서비스)
2) ERD 설계 - Spring Project(OTT 서비스)
3) 회원 가입 기능 구현 - Spring Project (OTT 서비스)
4) 로그인, 로그아웃 기능 구현 - Spring Project (OTT 서비스)
5) 상세 페이지 및 회원 정보 수정 - Spring Project (OTT 서비스)
6) CRUD를 한번에 → 게시판 만들기(QNA게시판) - Spring Project(Mybatis) (OTT 서비스)
7) 게시판 페이징 처리 - Spring Project (OTT 서비스)
8) 카카오 지도 API 사용하기 - Spring Project (OTT 서비스)
9) (네아로) 네이버 로그인 API 활용 사이트 로그인 및 회원가입 - SPRING Project(OTT 서비스)
10) 카카오 로그인 API 사용하기(내 사이트 로그인 및 회원가입) - Spring Project(OTT Service)
11) 아임포트(포트원) API를 이용한 결제처리 - Spring Project(OTT Service)
12) 관리자 페이지 만들기(데이터 통계 및 chart.js, 유저 알고리즘) - Spring Project(OTT Service)
13) 관리자 페이지 (영상 정보 업로드 시 여러 테이블에 insert 및 update) - Spring Project(OTT Service)
포스팅 당시 영상 상세페이지의 대댓글이 구현되지 않은 상황이었기 때문에, 사용자의 질문에 대한 관리자의 답변이 달렸을 경우 알림을 우선적으로 구현하고, 차후에 대댓글이 구현된 후 댓글 알림을 구현 할 예정이다.
설정하기
pom.xml
웹소켓의 데이터 통신은 내부적으로 JSON을 사용한다. JSON라이브러리는 기존에 추가되어 있기 때문에 생략한다.
<!-- Spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
servlet-context.xml
서블릿의 최상단의 beans:beans 내부에 아래의 코드를 추가하였고
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.3.xsd
알림 소켓 생성 시 매핑 값을 /echo로 설정할 것이고, 해당 요청을 EchoHandler에서 처리 할 것이다.
<!-- 웹소켓 -->
<beans:bean id="echoHandler" class="com.test.test1.chat.util.EchoHandler" />
<websocket:handlers>
<websocket:mapping handler="echoHandler" path="/echo"/>
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>
CDN추가(03-30 수정)
sockjs는 CDN을 추가하여 사용했다.
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
사용하기
TextWebSocketHandler를 상속받아 아래의 메서드를 사용할 수 있다.
afterConnectionEstablished - 연결 성공 시
afterConnectionClosed - 연결 종료 시
handleTextMessage - 메세지 송수신 시
답변 등록 시 ALARM 테이블에 아래와 같이 데이터가 추가된다.
등록 시 ALARM 테이블에 데이터가 입력되는 과정은 생략하도록 하겠다. 이제 이정도는 껌이다
IDX : PK
USER_ID : 알림 받을 사용자의 PK
CODE : 알림 정보 - 대댓글 알림 시 NewComment 등 CODE로 조건을 주어 VIEW를 다르게 할 수 있게 설계
CHECKED : default X이며 사용자가 알림 확인 시 자동으로 삭제처리된다.
PREFIX : 알림이 어디서 왔는지. 단순히 DB에 텍스트 형태로 입력시켜 주었다.
EchoHandler
package com.test.test1.chat.util;
(...생략...)
//Dao DI를위해 사용. 없을 경우 Dao = null
// -> Bean으로 등록되어 있어서 따로 스프링의 객체라고 주입시켜줘야함.
@Component
@RequestMapping("/echo")
public class EchoHandler extends TextWebSocketHandler{
@Autowired
private AlarmDao alarmDao;
public void setAlarmDao(AlarmDao alarmDao) {
this.alarmDao = alarmDao;
}
private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);
//로그인 한 인원 전체
private List<WebSocketSession> sessions = new ArrayList<WebSocketSession>();
//클라이언트가 웹 소켓 생성
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("Socket 연결");
//웹 소켓이 생성될 때마다 리스트에 넣어줌
sessions.add(session);
}
//JS에서 메세지 받을 때.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 메시지
for(WebSocketSession single : sessions) {
// 대댓글 때 사용
// String msg = message.getPayload();
// String[] str = msg.split(",");
// //JS에서 원하는대로 send하여 해당 기능 별 알람 구현
// //질문에 답변 달렸을 때(질문자 ID와 제목 들고옴)
// if(str != null && str.length == 2) {
// String id = str[0];
// String q_subject = str[1];
// int count = alarmDao.selectAlarmCount(id); //알람이 존재할 때
// }
//세션아이디
String hsid = (String) single.getAttributes().get("user_id");
//세션값이 같을때, 알람보낼 것이 있을 때만 전송 -> 로그인 한 사용자가 처음으로 알람 받는 순간임
//해당 sendMsg에 DB정보 넣어서 체크 안된 알람 전부 전송하기
if(single.getAttributes().get("user_id").equals(session.getAttributes().get("user_id"))) { //체크 안된 알림들만 담아서 View
List<AlarmDto> dto = new ArrayList<>();
dto = alarmDao.selectAlarm(hsid);
for(AlarmDto alarm : dto) {
int idx = alarm.getIdx();
String prefix = alarm.getPrefix();
String code = alarm.getCode();
if(code.equals("NewPost")) {
code = "답변이 등록되었습니다.";
}
TextMessage sendMsg = new TextMessage("("+idx+")" + prefix + "에 " + code);
single.sendMessage(sendMsg);
}
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {//연결 해제
// TODO Auto-generated method stub
logger.info("Socket 끊음");
//웹 소켓이 종료될 때마다 리스트에서 뺀다.
sessions.remove(session);
}
}
alarm.js
/* 02.23 장재호 */
const alarmUL = document.querySelector("#alarmUL");
const alarmI = document.querySelector("#alarmI");
const alarmDiv = document.querySelector("#alarmDiv");
var sock = null;
$(document).ready(function(){
connectWs();
});
//소켓
function connectWs(){
var ws = new SockJS("http://localhost:8080/echo");
sock = ws;
ws.onopen = function() {
console.log("연결완료");
ws.send($('#socketuserID').val());
};
ws.onmessage = function(event) {
/* 받을 알람이 있을 때 */
console.log(event.data);
if(event.data.length>0){
let newAlarm = '';
newAlarm += '<li scope="col">' + event.data + "</li>"
$('#alarmUL').append(newAlarm);
alarmDiv.style.visibility = "visible";
}
};
ws.onclose = function() {
console.log('close');
};
};
/* 알람창 추가 */
alarmI.addEventListener('click', function(){
alarmUL.classList.toggle('visible');
$(this).stop(false, false);
});
alarmUL.addEventListener('click', function(e){
var endIdx = e.target.textContent.indexOf(")");
var idx = e.target.textContent.substr(1, endIdx-1);
$.ajax({
url : '/alarmDel',
data : {"idx" : idx},
type : 'post',
success : function(data){
console.log(data);
alert("성공");
}
})
$(e.target).remove();
if(alarmUL.children.length == 0){
alarmDiv.style.visibility = "hidden";
}
})
/* *************************** */
실행되는 순서이다.
1. alarm.jsp를 include받은 페이지 접속 시 alarm.js에 설정된 소켓이 생성되며 EchoHandler의 아래 연결
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("Socket 연결");
//웹 소켓이 생성될 때마다 리스트에 넣어줌
sessions.add(session);
}
2. 소켓이 생성 될 때 아래의 send메서드를 통해 세션에 등록되어 있는 USER_ID가 전송된다.(input hidden)
ws.onopen = function() {
console.log("연결완료");
ws.send($('#socketuserID').val());
};
전송이 되면 아래의 EchoHandler의 메서드 중 handleTextMessage로 간다.
굳이 세션ID를 보내지 않고, ws.send()처리를 해도 됐다. 핸들러의 메서드를 실행만 시키면 됐었다.
왜냐, 서블릿 설정 시 HttpSession을 사용할 수 있는 인터셉터 설정을 하였기 때문이다.
message.getPayload()를 사용하여 JS에서 보낸 메세지를 출력할 수 있다. 만약 인터셉터를 사용하지 않고 세션값을 전송했을 경우, 해당 메서드를 사용하여 세션값을 뽑아 사용하면될 것이다.
//JS에서 메세지 받을 때.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 메시지
for(WebSocketSession single : sessions) {
//세션아이디
String hsid = (String) single.getAttributes().get("user_id");
//세션값이 같을때, 알람보낼 것이 있을 때만 전송 -> 로그인 한 사용자가 처음으로 알람 받는 순간임
//해당 sendMsg에 DB정보 넣어서 체크 안된 알람 전부 전송하기
if(single.getAttributes().get("user_id").equals(session.getAttributes().get("user_id"))) { //체크 안된 알림들만 담아서 View
List<AlarmDto> dto = new ArrayList<>();
dto = alarmDao.selectAlarm(hsid);
for(AlarmDto alarm : dto) {
int idx = alarm.getIdx();
String prefix = alarm.getPrefix();
String code = alarm.getCode();
if(code.equals("NewPost")) {
code = "답변이 등록되었습니다.";
}
TextMessage sendMsg = new TextMessage("("+idx+")" + prefix + "에 " + code);
single.sendMessage(sendMsg);
}
}
}
}
여튼 인터셉터를 사용했기 때문에, single에서 getAttribute를 통해 세션에 있는 유저 아이디를 가져올 수 있다.
해당 유저아이디를 바탕으로 조회하여 ALARM 테이블에 있는 정보를 가져와서 알람이 있을 경우 다시 JS를 통해 View에 출력 할 수 있다.
3. 전송된 무언가가 있을 경우
ws.onmessage = function(event) {
/* 받을 알람이 있을 때 */
if(event.data.length>0){
let newAlarm = '';
newAlarm += '<li scope="col">' + event.data + "</li>"
$('#alarmUL').append(newAlarm);
alarmDiv.style.visibility = "visible";
}
};
전송된 무언가가 있을 경우, newAlarm 변수 안에 리스트 형태로 알람 값을 담아주었다.
alarmDiv는 알람 버튼이 있는 디비전을 말하며, 알람이 있을 경우 종 모양 아이콘이 화면에 나타난다.
종 모양 아이콘을 클릭할 경우 알림 내역들이 보일 수 있게 설계하였다.
예시로 관리자가 질문을 등록하고 관리자가 답변을 달았을 때 알림이 생기는 것을 담았다. 로그인 로그아웃 질문등록 이런 과정들이 시간이 길어 GIF가 깨져서 그러니 양해바란다.
갑자기 난이도가 훅 올라간 느낌이여서 어려웠지만 소켓을 다루다보니 너무 재밌었다. 조금 더 심도있게 공부해도 재밌을 것 같다
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!