let socket
= new WebSocket("ws://localhost:8080/ws/chat?userNo="+senderNo+"&roomNo="+roomNo);
package com.gn.mvc.websocket;
import java.util.HashMap;
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.gn.mvc.dto.ChatMsgDto;
import com.gn.mvc.entity.ChatMsg;
import com.gn.mvc.entity.ChatRoom;
import com.gn.mvc.entity.Member;
import com.gn.mvc.repository.ChatMsgRepository;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class ChatWebSocketHandler extends TextWebSocketHandler {
// new 연산자 있기 때문에 Bean이 아니어서 requiredConstructor 쓸 필요 없다. 그냥 전역 변수일 뿐이다.
private static final Map<Long, WebSocketSession> userSessions = new HashMap<Long, WebSocketSession>();
// roomNo을 쌓는 애를 만들자
private static final Map<Long, Long> userRooms = new HashMap<Long, Long>();
// DB에 저장하기 위해 ChatMsgRepository + Lombok의 도움을 받자
private final ChatMsgRepository chatMsgRepository;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 쿼리 스트링 떼와서 쓰는 법!
// 1. ws://localhost:8080/ws/chat?senderNo=3
// 2. senderNo=3
// 3. 0번 인덱스 -> senderNo, 1번 인덱스 -> 3
//String senderNo = session.getUri().getQuery().split("=")[1];
// 이제는 두가지 정보 필요!(userNo, roomNo)
String userNo = getQueryParam(session, "senderNo");
String roomNo = getQueryParam(session, "roomNo");
userSessions.put(Long.parseLong(userNo), session);
// 위 아래의 차이를 잘 알아보자
// 연결된 사람이 누구고 몇번 채팅방을 사용하느냐!
userRooms.put(Long.parseLong(userNo), Long.parseLong(roomNo));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
// 현제 message 안에 JSON 형태로 되어있다. 파싱 해보자!
ObjectMapper objectMapper = new ObjectMapper();
// message.getPayload() 제이슨 형태로 받아오는?? 그래서 아래처럼 파싱한다.
ChatMsgDto dto = objectMapper.readValue(message.getPayload(), ChatMsgDto.class);
// 이 사람이 연결되어 있는 상황이라면~ 뜻!
if(userSessions.containsKey(dto.getSender_no())) {
// 화면에 응답해주기 전에 DB에 채팅 메시지 등록
// 1. dto -> entity
Member member = Member.builder()
.memberNo(dto.getSender_no())
.build();
ChatRoom chatRoom = ChatRoom.builder()
.roomNo(dto.getRoom_no())
.build();
ChatMsg entity = ChatMsg.builder()
.sendMember(member)
.chatRoom(chatRoom)
.msgContent(dto.getMsg_content())
.build();
// 2. entity save
chatMsgRepository.save(entity);
}
// 내가 썼지만 상대방도, 나도 기록이 남아야하니까~
WebSocketSession receiverSession = userSessions.get(dto.getReceiver_no());
// && receiverRoom == dto.getRoom_no() 받는 사람이 채팅방 연결이 어디에 되어있는가? 확인하는 코드 필요. 방의 정보도 받아와야한다.
// messagePayload와 맞는지 판단한다.
// dto.getReceiver_no() 받는 사람의 정보(PK)
Long receiverRoom = userRooms.get(dto.getReceiver_no());
if(receiverSession != null && receiverSession.isOpen() && receiverRoom == dto.getRoom_no()) {
// receiverSession.sendMessage(new TextMessage(dto.getMsg_content()));
// 메시지 JSON 데이터 전달
receiverSession.sendMessage(new TextMessage(message.getPayload()));
}
WebSocketSession senderSession = userSessions.get(dto.getSender_no());
Long senderRoom = userRooms.get(dto.getSender_no());
// 보낸 사람이 똑같은 채팅방 위치에 있는가?
if(senderSession != null && senderSession.isOpen() && senderRoom == dto.getRoom_no()) {
senderSession.sendMessage(new TextMessage(message.getPayload()));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// String userNo = session.getUri().getQuery().split("=")[1];
String userNo = getQueryParam(session, "senderNo");
userSessions.remove(Long.parseLong(userNo));
userRooms.remove(Long.parseLong(userNo));
}
// 기능 : WebSocketSession의 URL 파싱
// 파라미터 : url, key값
// 반환값 : value값
/*
* author : 김가남
* history : 2025-03-24
* param : url, key data
* return : value data
* role : WebsocketSession url parsing
* 이런식으로 쓰면 친절하기는 한데 너무 오래 걸릴 것 같다.
*/
// 메소드 호출, 사용방법!
// getQueryParam(session, "senderNo"); -> 3
// getQueryParam(session, "roomNo"); -> 1
private String getQueryParam(WebSocketSession session, String key) {
// 쿼리스트링 가져온 것
// senderNo=3&roomNo=1
// 만약 더 있다면? 숫자만 조금 바꿔서 쓰기 좋음
String query = session.getUri().getQuery();
if(query != null) {
String[] arr = query.split("&");
// 0번 인덱스 : senderNo=3
// 1번 인덱스 : roomNo=1
for(String target : arr) {
String[] keyArr = target.split("=");
if(keyArr.length == 2 && keyArr[0].equals(key)) {
return keyArr[1];
}
}
}
return null;
}
}
내가 보낸 채팅은 오른쪽 정렬
상대방이 보낸 채팅은 왼쪽 정렬
receiverSession.sendMessage(new TextMessage(dto.getMsg_content()));
String payload = message.getPayload();
receiverSession.sendMessage(new TextMessage(message.getPayload()));
<script>
let senderNo = document.getElementById("senderNo").value;
// roomNo를 전역변수로 뺄 수 있는 이유는~ 이 페이지에 진입하는 순간 고정되는 변수이기 때문이다.
// 채팅 메시지 같은 변화하는 + 클릭 이벤트에 바뀌는 값들은 전역변수로 사용하면 안된다.
let roomNo = document.getElementById('roomNo').value;
// 채팅방 정보X -> 필터링 문제 발생
// let socket = new WebSocket("ws://localhost:8080/ws/chat?senderNo="+senderNo);
// 연결된 사람 누구인지~ + 채팅방이 몇번 채팅방입니까~ 해야한다.
// 연결 시점에 채팅방 정보 전달ㄷ
let socket = new WebSocket("ws://localhost:8080/ws/chat?senderNo="+senderNo+"&roomNo="+roomNo);
const sendMsg = function(){
// 채팅방, 받는 사람, 보내는 사람, 메시지
let receiverNo = document.getElementById('receiverNo').value;
let msgContent = document.getElementById('msgContent').value;
if(msgContent != ''){
socket.send(JSON.stringify({
sender_no : senderNo,
room_no : roomNo,
receiver_no : receiverNo,
msg_content : msgContent
}));
}
document.getElementById('msgContent').value = '';
}
socket.onmessage = function(event){
// 이거는 그냥 문자로 받아버린 것.
// document.getElementById('chatBox').innerHTML += "<p>" + event.data + "</p>";
// 이제는 JSON 형태로 받아서. 원하는 형태로 자료형을 바꿔보자.
let msgData = JSON.parse(event.data);
// 1. 채팅 출력 div에 접근
let chatBox = document.getElementById("chatBox");
// 2. 채팅 하나하나 만들어주기
let msgDiv = document.createElement("div");
msgDiv.classList.add("msg-box");
// 메세지를 보낸 사람이랑, 현재 사람이랑 똑같다면~ = 내가 보낸 거라면!
if(msgData.sender_no == senderNo){
msgDiv.classList.add("msg-right");
} else{
msgDiv.classList.add("msg-left");
}
// 3. 채팅 출력 div에 채팅 하나씩 넣기
msgDiv.innerHTML = `<div>${msgData.msg_content}</div>`;
chatBox.appendChild(msgDiv);
// 4. 스크롤 밑으로 내리기
// $('#chatBody').scrollTop($('#chatBody')[0].scrollHeight);
chatBox.scrollTop = chatBox.scrollHeight;
}
</script>
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
// 현제 message 안에 JSON 형태로 되어있다. 파싱 해보자!
ObjectMapper objectMapper = new ObjectMapper();
// message.getPayload() 제이슨 형태로 받아오는?? 그래서 아래처럼 파싱한다.
ChatMsgDto dto = objectMapper.readValue(message.getPayload(), ChatMsgDto.class);
// 이 사람이 연결되어 있는 상황이라면~ 뜻!
if(userSessions.containsKey(dto.getSender_no())) {
// 화면에 응답해주기 전에 DB에 채팅 메시지 등록
// 1. dto -> entity
Member member = Member.builder()
.memberNo(dto.getSender_no())
.build();
ChatRoom chatRoom = ChatRoom.builder()
.roomNo(dto.getRoom_no())
.build();
ChatMsg entity = ChatMsg.builder()
.sendMember(member)
.chatRoom(chatRoom)
.msgContent(dto.getMsg_content())
.build();
// 2. entity save
chatMsgRepository.save(entity);
}
// 내가 썼지만 상대방도, 나도 기록이 남아야하니까~
WebSocketSession receiverSession = userSessions.get(dto.getReceiver_no());
// && receiverRoom == dto.getRoom_no() 받는 사람이 채팅방 연결이 어디에 되어있는가? 확인하는 코드 필요. 방의 정보도 받아와야한다.
// messagePayload와 맞는지 판단한다.
// dto.getReceiver_no() 받는 사람의 정보(PK)
Long receiverRoom = userRooms.get(dto.getReceiver_no());
if(receiverSession != null && receiverSession.isOpen() && receiverRoom == dto.getRoom_no()) {
// receiverSession.sendMessage(new TextMessage(dto.getMsg_content()));
// 메시지 JSON 데이터 전달
receiverSession.sendMessage(new TextMessage(message.getPayload()));
}
WebSocketSession senderSession = userSessions.get(dto.getSender_no());
Long senderRoom = userRooms.get(dto.getSender_no());
// 보낸 사람이 똑같은 채팅방 위치에 있는가?
if(senderSession != null && senderSession.isOpen() && senderRoom == dto.getRoom_no()) {
senderSession.sendMessage(new TextMessage(message.getPayload()));
}
}
SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
DELIMITER //
CREATE TRIGGER `chat_room_trigger` AFTER INSERT ON `chat_message` FOR EACH ROW BEGIN
UPDATE `chat_room`
SET last_msg = NEW.message_content,
last_date = NEW.send_date
WHERE room_no = NEW.room_no;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;
<div id="chatBox">
<div th:if="${!#lists.isEmpty(msgList)}"
class="msg-box" th:each="chat : ${msgList}"
th:classappend="${chat.sendMember.memberNo == #authentication.principal.member.memberNo} ? 'msg-right' : 'msg-left'"
th:text="${chat.msgContent}">
</div>
</div>
1) AOP(Aspect Orientied Programming)
2) Aspect
3) execution
execution([접근제한자] 리턴타입 [클래스명].메소드명(매개변수))
ex) private한 애들한테만 적용할게요~ 가능!
ex) 반환형이 String인 애들한테만 적용할게요~ 가능!
ex) 메소드명이 list로 끝나는 애들한테만 적용할게요~ 가능!
execution(public String com.gn.mvc..*.*List(*))
package com.gn.mvc;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LoggerAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspect.class);
@Before("execution(* com.gn.mvc..*(..))")
public void methodBefore(JoinPoint jp) {
String className = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName(); // 시그니처라 aspect를 뜻하는 거다?
LOGGER.warn(className + "의 " + methodName + "실행 직전");
}
}
1) 스케줄링 관련 클래스를 만들고 @EnableScheduling 어노테이션 추가 + @Configuration 어노테이션 추가
2) 그 안에 메소드 추가하면서 메소드에는 @Scheduled(표현식) 어노테이션 추가
3) 다양한 표현식 사용 가능.
4) Spring이 호출해서 쓰는 것이지 우리가 직접 메소드 호출해서 쓰는 것이 아니다.
package com.gn.mvc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@EnableScheduling
@Configuration
public class SchedulerConfig {
// 1. fixedRate(이전 작업 시작 시점)
@Scheduled(fixedRate=5000)
public void runTask1() {
System.out.println("5초마다 실행~!!");
System.out.println(System.currentTimeMillis());
}
// 2. fixedDelay(이전 작업 종료 시점)
@Scheduled(fixedDelay = 3000)
public void runTask2() {
System.out.println(System.currentTimeMillis());
}
// 3. initialDelay
@Value("${schedular.enable}")
private boolean isSchedulerEnabled;
// 4. cron식
@Scheduled(cron="0 50 15 * * *")
public void runTask4() {
if(!isSchedulerEnabled) {
return;
}
System.out.println("매일 3시 50분에 실행");
}
}
fixedRate
이전 작업의 시작 시점으로부터 고정된 간격으로 실행
실행 간격은 밀리초 단위
fixedDelay
이전 작업의 종료 시점부터 고정된 간격으로 실행
실행 간격은 밀리초 단위
initialDelay
애플리케이션이 시작된 후 일정 시간 후에 실행 시작
fixedRate 또는 fixedDelay와 함께 사용 가능
ex) @Scheduled(initialDelay = 3000, fixedRate = 5000)
애플리케이션 시작 -> 3초 후 initialDelay 동작 -> fixedRate initialDelay 끝나고 5초뒤 동작?
cron 표현식 사용
정교한 스케줄링이 필요한 경우 사용
총 6자리의 숫자가 띄어쓰기를 기준으로 나눠져있음
0 * * * * 매분 0초에 실행
0 0 12 * * 매일 정오 12시에 실행
0 0 0 25 12 ? 매년 크리스마스 자정에 실행
물음표는 필드 중 하나를 설정하지 않을 때 사용
로그 또는 출력문이 너무 많이 나오는 경우
스케줄러를 일시적으로 중지하고 싶은 경우
scheduler.enable=false
// 4. cron식
@Scheduled(cron="0 50 15 * * *")
public void runTask4() {
if(!isSchedulerEnabled) {
return;
}
System.out.println("매일 3시 50분에 실행");
}
다시 활성화 하는 경우에는 application.properties에서 스케줄러 활성화 true로 변경