본문 바로가기

국비교육/국비교육

day76 - 1115

** 준비 - 로그인 구현

PageController

@Controller
@RequestMapping("/page")
public class PageController {
	
	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	// 홈 Mapping
	@GetMapping("/home")
	public String home() {
		return "home";
	}
	
	// 로그인 Mapping
	@PostMapping("/login")
	public String login(@ModelAttribute MemberDto memberDto, HttpSession session) {
		// 입력받은 회원 아아디로 단일 조회
		MemberDto findDto = sqlSession.selectOne("member.get", memberDto.getMemberId());
		// 입력한 아이디를 갖는 회원이 없을 경우
		if(findDto == null) {
			// 홈 Mapping으로 강제 이동(redirect)
			return "redirect:home";
		}
		
		// 입력한 비밀번호가 조회한 회원 정보의 비밀번호와 일치하는지 여부
		boolean judge = memberDto.getMemberPw().equals(findDto.getMemberPw());
		
		// 입력한 비밀번호가 조회한 회원 정보의 비밀번호와 일치한다면
		if(judge) {
			// HttpSession에 해당 회원의 아이디, 닉네임, 회원등급 저장
			session.setAttribute("loginId", findDto.getMemberId());
			session.setAttribute("loginNick", findDto.getMemberNick());
			session.setAttribute("loginAuth", findDto.getMemberGrade());
		}
		// 홈 Mapping으로 강제 이동(redirect)
		return "redirect:home";
	}
	
	// 로그아웃 Mapping
	@GetMapping("/logout")
	public String logout(HttpSession session) {
		// 세션 무효화
		session.invalidate();
		// 홈 페이지(home.jsp)로 연결
		return "redirect:home";
	}
}

 

home.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri = "http://java.sun.com/jsp/jstl/core" %>

<h1>웹소켓 예제 모음</h1>

<!-- 로그인/로그아웃 화면 -->
<c:choose>
	<c:when test = "${loginId == null}">
		<form action = "login" method = "post">
			<input type = "text" name = "memberId">
			<input type = "password" name = "memberPw">
			<button type = "submit">로그인</button>
		</form>
	</c:when>
	<c:otherwise>
		<h2><a href = "logout">로그아웃</a></h2>
	</c:otherwise>
</c:choose>

<!-- 상태 판정 출력 -->
<h2>아이디 : ${loginId}</h2>
<h2>닉네임 : ${loginNick}</h2>
<h2>권한 : ${loginAuth}</h2>

<!-- 모든 예제로 이동할 수 있는 링크 -->
<h2><a href = "basic">기본 연결</a></h2>
<h2><a href = "multi">다중 사용자</a></h2>
<h2><a href = "message">텍스트 메시지</a></h2>
<h2><a href = "json">JSON 메시지</a></h2>
<h2><a href = "sockjs">SockJS</a></h2>

 


웹소켓을 이용한 실시간 채팅 구현(4) - 로그인한 회원 정보 표시

** HttpSessionHandshakeInterceptor의 동작

- HttpSession에 저장된 값을 WebSocketSession으로 넘겨준다

 

MemberWebSocketServer

@Slf4j
@Service
public class MemberWebsocketServer extends TextWebSocketHandler {

	// 웹소켓 사용자 저장소 - Set(중복 방지)
	private Set<WebSocketSession> users = new CopyOnWriteArraySet<>(); // 동기화된 Set

	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		
		// HttpSessionHandshakeInterceptor의 동작을 확인하기 위한 로그 출력
		log.debug("session = {}", session.getAttributes());
		
		// HttpSessionHandshakeInterceptor에 의해 HttpSession에서 WebSocketSession로 가져온 값을 Map 형태로 반환
		Map<String, Object> attributes = session.getAttributes();
		
		// Map에 저장된 값 반환
		String loginId = (String)attributes.get("loginId");
		String loginNick = (String)attributes.get("loginNick");
		String loginAuth = (String)attributes.get("loginAuth");
		
		// Map에서 반환한 값 확인
		log.debug("{}, {}, {}", loginId, loginNick, loginAuth);
		
		// 사용자 저장소에 해당 사용자 추가
		users.add(session);
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		// WebSocketSession의 정보 출력
		log.debug("session = {}", session);
		// 사용자 저장소에서 해당 사용자 삭제
		users.remove(session);
	}
	
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		
		// 로그인한 회원의 로그인 정보 반환
		Map<String, Object> attributes = session.getAttributes();
		String loginId = (String)attributes.get("loginId");
		String loginNick = (String)attributes.get("loginNick");
		String loginAuth = (String)attributes.get("loginAuth");
		
		// 반환된 정보를 확인하기 위한 로그 출력
		log.debug("{}, {}, {}", loginId, loginNick, loginAuth);
		
		// 회원인지(로그인한 상태인지) 판정
		boolean available = loginAuth != null;
		
		// 비회원이라면
		if(available == false) {
			log.warn("비회원 채팅 금지");
			return;
		}
		
		// 회원이 보낸 메시지의 전송 데이터 확인을 위한 로그 출력
		log.debug("메세지 - {}", message.getPayload());

		// ObjectMapper의 인스턴스 생성 
		ObjectMapper mapper = new ObjectMapper();
		
		// 1. JSON을 Java Object로 변환 - readValue()
		// 1) JSON을 Map 형태로 변환
		//Map json = mapper.readValue(message.getPayload(), Map.class);
		//log.debug("json = {}", json);
		
		// 2) JSON을 클래스의 인스턴스 형태로 변환
		MessageVO json = mapper.readValue(message.getPayload(), MessageVO.class);
		log.debug("json = {}", json);

		// VO에 시간 설정
		json.setTime(new Date());
		
		// VO에 로그인 정보(id, nickname, auth) 설정
		json.setId(loginId);
		json.setNickname(loginNick);
		json.setAuth(loginAuth);

		// 2. Java Object를 JSON 형태로 변환 - writeValue()
		// 설정된 VO를 JSON 형태의 문자열로 변환
		String payload = mapper.writeValueAsString(json);
		// JSON 형태의 문자열로 메시지 생성
		TextMessage jsonMessage = new TextMessage(payload);

		// 연결된 모든 사용자에게 메시지 전송
		for(WebSocketSession user : users) {
			//user.sendMessage(json);
			user.sendMessage(jsonMessage);
		}
	}
}

 

WebSocketServerConfiguration

- HttpSessionHandshakeInterceptor는 HttpSession의 값을 WebSocketSession로 넘겨준다

@Configuration
@EnableWebSocket // 웹소켓 활성화
public class WebSocketServerConfiguration implements WebSocketConfigurer {

	// 의존성 주입
	@Autowired
	private MemberWebsocketServer memberWebsocketServer;
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

		// HttpSessionHandshakeInterceptor는 HttpSession을 WebSocketSession으로 넘겨준다
		registry.addHandler(memberWebsocketServer, "/ws/member")
				.addInterceptors(new HttpSessionHandshakeInterceptor())
				.withSockJS();
	}	
}

 

PageController

@Controller
@RequestMapping("/page")
public class PageController {
	
	@GetMapping("/member")
	public String member() {
		return "member";
	}
}

 

member.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<style>
	.chat-message {
		padding: 1em;
		border: 1px solid black;
		border-radius : 1em;
	}
</style>

<h1>회원 채팅 예제</h1>
<button class="btn-connect">연결</button>
<button class="btn-disconnect">종료</button>

<hr>

<input type="text" id="message-input">
<button type="button" id="message-send">전송</button>

<hr>

<div id="message-list"></div>

<!-- Moment CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>

<!-- SockJS CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>

<!-- JQuery CDN -->
<script src="https://code.jquery.com/jquery-3.6.1.js"></script>

<script>
	$(function(){
		// 초기 웹소켓은 연결 해제 상태
		disconnectState();
		
		// 1. 연결버튼을 누를 때 - 웹소켓 연결 생성
		$(".btn-connect").click(function(){
			// 웹소켓 연결 주소 설정
			// - SockJS를 사용하면 주소를 http로 시작하도록 할 수 있다
			var uri = "${pageContext.request.contextPath}/ws/member";
			
			// 웹소켓 연결 생성
			// - 접속시 WebSocket이 아닌 SockJS 객체를 생성
			socket = new SockJS(uri); // 변수로 만들지 말고 window에 붙여서 만듬
			
			// 웹소켓에 대한 이벤트 설정
			// 1) 웹소켓 연결이 생성될 때
			socket.onopen = function(){
				console.log("open");	
				connectState();	// 연결 상태일 때 connectState() 실행
			};
			
			// 2) 웹소켓 연결 중 연결이 해제될 때
			socket.onclose = function(){
				console.log("close");
				disconnectState(); // 해제 상태일 때 disconnectState() 실행
			};
			
			// 3) 웹소켓 연결 중 연결에 에러가 발생할 때
			socket.onerror = function(){
				console.log("error");
				disconnectState(); // 에러 상태일 때 disconnectState() 실행
			};
			
			// 4) 웹소켓 연결 중 메시지가 수신될 때
			socket.onmessage = function(e){
				// 문자열을 JSON 형태로 수신 - JSON.parse()
				var data = JSON.parse(e.data);
				//console.log(data);
				
				// p 태그 지정 - CSS에서 디자인을 하기 위해 클래스 부여
				var p = $("<p>").addClass("chat-message");
				
				// p 태그 지정 - 닉네임[등급] 형태
				var w = $("<p>").text(data.nickname + "[" + data.auth + "]");
				
				// 수신한 JSON의 time 형태 변경 
				var time = moment(data.time).format("YYYY-MM-DD hh:mm");
				
				// p 태그 지정 - (시간) 형태
				var t = $("<p>").text("(" + time + ")");
				
				// p 태그 지정 - 메시지 표시
				var c = $("<p>").text(data.text);
				
				// 지정된 p 태그들을 연결
				p.append(w).append(c).append(t);
				
				// 메시지를 표시하는 div 안에 연결된 p 태그 생성
				$("#message-list").append(p);
				
				// 스크롤 하단으로 이동
				var height = $(document).height();
				$(window).scrollTop(height);
			};
			
		});
		
		// 2. 종료 버튼을 누를 때 - 웹소켓 연결 해제
		$(".btn-disconnect").click(function(){
			//웹소켓 연결 종료
			//window.socket.close();
			socket.close();
			
		});
		
		// 3. 전송 버튼을 누를 때 - 웹소켓에 연결된 모든 사용자에게 메시지 전달
		$("#message-send").click(function(){
			// 입력창의 값 지정
			var text = $("#message-input").val();
			// 입력창에 값이 입력되지 않았을 경우 return
			if(text.length == 0) return;
			
			// JSON으로 변환해서 전송
			// - JSON.stringify(객체) : 객체를 문자열로
			// - JSON.parse(문자열) : 문자열을 객체로
			
			// 입력값을 JSON 형태로 설정
			var data = {
				text : text
			};
			
			// 입력창의 값 전송
			socket.send(JSON.stringify(data));
			
			// 전송 후 입력창 초기화(비우기)
			$("#message-input").val("");
		});
		
		// 웹소켓 연결이 만들어진 상태일 때
		function connectState(){
			$(".btn-connect").prop("disabled", true); // 연결버튼 비활성화
			$(".btn-disconnect").prop("disabled", false); // 종료버튼 비활성화 해제
		}
		
		// 웹소켓 연결이 해제된 상태일 때
		function disconnectState(){
			$(".btn-connect").prop("disabled", false); // 연결버튼 비활성화 해제
			$(".btn-disconnect").prop("disabled", true); // 종료버튼 비활성화
		}
	});
</script>

 


 

웹소켓을 이용한 실시간 채팅 구현(5) - 그룹 채팅

그룹 채팅의 구조

User

- 웹소켓에 연결된 모든 사용자는 WebSocketSession을 가지고 있다

- 웹소켓 서버에서 수신한 메시지를 특정 사용자에게 보내는 메소드 존재

@Data 
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

	// 사용자 정보
	private String memberId; // 사용자 아이디
	private String memberNick; // 사용자 닉네임
	private String memberGrade; // 사용자 등급
	
	private WebSocketSession session; // 사용자의 WebSocketSession
	
	// 사용자 아이디 비교
	public boolean is(String memberId) {
		// 매개변수로 입력된 memberId가 null이면 false를 반환
		if(memberId == null) return false;
		
		// User 클래스의 인스턴스의 memberId 값이 null이면 false를 반환
		if(this.memberId == null) return false;
		
		// User 클래스의 인스턴스의 memberId 값이 매개변수로 입력된 memberId와 같은지 판정
		// - 특정 방에 참가하고있는 모든 참가자 중 해당 아이디를 가진 참가자 검색을 위함
		return this.memberId.equals(memberId);
	}
	
	// 메시지 전송
	// - 웹소켓 연결을 통해 웹소켓 서버가 수신한 메시지를 사용자에게 전송
	public void send(TextMessage message) throws IOException {
		session.sendMessage(message);
	}
}

 

Room

- User 객체를 저장하는 방 참가자 저장소 존재

- enter(User user) : 사용자가 방 입장시 방 참가 저장소에 해당 사용자의 User 객체 저장

- leave(User user) : 사용자가 방 퇴장시 방 참가 저장소에서 해당 사용자의 User 객체 제거

- broadcast(TextMessage message) : 웹소켓 서버에서 수신한 메시지를 특정 방에 참가한 모든 사용자에게 전송

  (방 참가 저장소에 메시지를 보낸 회원의 User 객체가 포함된 방의 이름을 찾아 해당 방의 모든 참가자에게 메시지 전송)

@Slf4j
public class Room {

	// 방 참가자 저장소 - Set (중복 방지)
	// - 특정 방에 참가한 모든 사용자의 User 클래스의 인스턴스 저장
	private Set<User> users = new CopyOnWriteArraySet<>();
	
	// 방 입장
	public void enter(User user) {
		// 방 참가자 저장소에 해당 사용자를 추가
		users.add(user);
		log.debug("--> 방 입장 : {}", user);
	}
	
	// 방 퇴장
	public void leave(User user) {
		// 방 참가자 저장소에서 해당 사용자를 제거
		users.remove(user);
		log.debug("--> 방 퇴장 : {}", user);
	}
	
	// 방 참가자 중 특정 아이디를 가진 사용자가 존재하는지 여부 반환
	public User search(String memberId) {
		for(User user : users) { // 방 참가자 저장소에 아이디가 저장된 모든 사용자에 대해
			// 방 참가자 저장소에 입력된 memberId를 아이디로 하는 사용자이 있으면 
			if(user.is(memberId)) return user; // 해당 사용자의 User 클래스의 인스턴스 반환
		}
		// 기본적으로 null을 반환
		return null;
	}
	
	// 방 참가자 수 반환
	public int size() {
		// 방 참가자 저장소의 길이(참가자 수) 반환
		return users.size();
	}
	
	// 메시지 전송
	// - 웹소켓 연결을 통해 웹소켓 서버에서 방 참가자 저장소에 아이디가 저장된 모든 사용자에게 메시지 전송
	public void broadcast(TextMessage message) throws IOException {
		for(User user : users) { // 방 참가자 저장소에 아이디가 저장된 모든 사용자에 대해
			user.send(message); // 웹소켓 서버가 수신한 메시지를 전송
		}
	}
}

 

Channel

- Room 객체를 저장하는 방 저장소 존재

- join(User user, String name) : 특정 이름(name)의 Room 객체를 반환하여 Room 클래스의 enter(User user) 메소드 실행

- exit(User user, String name) : 특정 이름(name)의 Room 객체를 반환하여 Room 클래스의 leave(User user) 메소드 실행

- send(User user, TextMessage message) : 메시지를 전송한 사용자가 포함된 방 이름을 반환하여 해당 방의 방 저장소에서 해당 방의 Room 객체를 반환한 후 Room 클래스의 broadcast(TextMessage message) 메소드 실행

public class Channel {

	// 방 저장소 
	// - 방 이름으로 Room 클래스의 인스턴스를 관리하는 저장소
	// - 동기화된 Map 사용
	Map<String, Room> rooms = Collections.synchronizedMap(new HashMap<>());
	
	// 방 입장
	public void join(User user, String name) {
		if(!rooms.containsKey(name)) { // 해당 이름(name)의 방이 없으면
			rooms.put(name, new Room()); // 해당 이름으로 방을 생성
		}
		// 특정 이름(name)의 방에 특정 사용자(user) 입장
		// - rooms.get(name)까지의 결과로 해당 이름(name)의 Room 클래스의 인스턴스가 반환된다
		rooms.get(name).enter(user);
	}
	
	// 방 이름 반환
	// - 특정 사용자가 참가하고 있는 방의 이름 반환
	public String find(User user) {
		// 방 저장소의 Key인으로 다음 작업을 수행
		for(String name : rooms.keySet()) { // keySet() - Map의 Key만 반환
			// 방 저장소에서 Room 클래스의 인스턴스를 반환
			Room room = rooms.get(name);
			// 반환한 인스턴스의 방 참가자 저장소에서 특정 아이디를 가진 참가자가 존재하면
			if(room.search(user.getMemberId()) != null) {
				// 해당 방의 이름을 반환
				return name;
			}
		}
		// 기본적으로 null을 반환
		return null;
	}
	
	// 방 퇴장
	public void exit(User user) {
		// 특정 사용자가 참가하고 있는 방의 이름을 반환
		String name = this.find(user);
		// 해당 이름의 방에서 퇴장
		// - rooms.get(name)의 결과로 해당 이름(name)을 가진 Room 클래스의 인스턴스 반환
		rooms.get(name).leave(user);
		// 해당 이름의 방에 참가하고 있는 참가자 수가 0인 경우
		if(rooms.get(name).size() == 0) {
			// 해당 이름의 방 제거
			rooms.remove(name);
		}
	}
	
	// 특정 방의 참가자 전체에게만 메시지 전송
	public void send(User user, TextMessage message) throws IOException {
		// 특정 사용자가 참가하고 있는 방의 이름을 반환
		String name = this.find(user);
		// 참가하고 있는 방이 있다면 (name이 null이 아닌 경우)
		if(name != null) {
			// 해당 이름의 방에 메시지 전송
			// - rooms.get(name)의 결과로 해당 이름(name)을 가진 Room 클래스의 인스턴스 반환
			// - broadcast(message) 메소드는 해당 방 참가자 저장소에 아이디가 저장된 모든 사용자에게 메시지 전송
			rooms.get(name).broadcast(message);
		}
	}
}

 

MemberGroupChatServer

- 웹소켓 서버에는 기본적으로 WaitingRoom과 Channel 객체가 존재

- WaitingRoom 객체는 신규로 웹소켓 연결이 생성된 사용자의 User 객체를 일시적으로 저장

- Channel 객체는 Room 객체를 저장

 

1) 웹소켓 연결 생성시

- 웹소켓 연결이 생성되면 신규로 연결된 사용자의 WebSocketSession을 이용하여 User 객체 생성

- User 객체 생성 후 WaitingRoom의 방 참가자 저장소에 User 객체를 저장

 

2) 웹소켓 연결 해제시

- 웹소켓 연결이 해제되면 해제된 사용자의 WebSocketSession을 이용하여 User 객체 생성

- User 객체를 통해 Channel에 포함된 Room 및 WaitingRoom의 방 참가자 저장소에서 해당 User 객체를 제거

 

3) 웹소켓 서버로 메시지 전송시

- 사용자가 특정 방 참가를 위한 메시지인지, 특정 방에 메시지를 전송하기 위한 메시지인지 판정

- 방 참가를 위한 메시지인 경우 해당 사용자의 User 객체를 WaitingRoom의 방 참가자 저장소에서 제거하고

  참가하려는 방의 방 참가자 저장소에 추가

- 특정 방에 메시지 전송을 위한 메시지인 경우 메시지에 필요한 정보를 추가하여 웹소켓 서버에서

  해당 방에 참가한 모든 사용자에게 메시지 전송

@Slf4j
@Service
public class MemberGroupChatServer extends TextWebSocketHandler {
	
	// 대기실 
	// - 회원의 웹소켓 연결이 생성되면 대기실 참가자 저장소에 해당 회원을 참가자로서 저장
	private Room waitingRoom = new Room();
	
	// 채널
	// - 방들의 이름이 저장된 저장소를 포함
	private Channel channel = new Channel();
	
	// 웹소켓 연결 신규 생성시
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		
		// WebSocketSession에 저장된 값을 Map 형태로 반환 
		// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
		Map<String, Object> attr = session.getAttributes();
		
		// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
		User user = User.builder()
					.memberId((String)attr.get("loginId"))
					.memberNick((String)attr.get("loginNick"))
					.memberGrade((String)attr.get("loginAuth"))
					.session(session)
				.build();
		
		// 신규 참가자 대기실 입장
		// - 웹소켓 연결이 생성된 회원의 User 클래스의 인스턴스를 Room 클래스의 방 참가자 저장소인 Set<User> users에 추가
		waitingRoom.enter(user);
		
		// 대기실 인원 확인을 위한 로그 출력
		log.debug("대기실 입장 - 현재 {}명", waitingRoom.size());
	}
	
	// 웹소켓 연결 해제시
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		
		// WebSocketSession에 저장된 값을 Map 형태로 반환 
		// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
		Map<String, Object> attr = session.getAttributes();
		
		// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
		User user = User.builder()
					.memberId((String)attr.get("loginId"))
					.memberNick((String)attr.get("loginNick"))
					.memberGrade((String)attr.get("loginAuth"))
					.session(session)
				.build();
		
		// 참가자 채널 퇴장
		// - 웹소켓 연결이 해제된 회원이 참가하고있던 방 이름을 찾아 해당 방의 방 참가자 저장소인 Set<User> users에서 해당 회원의 User 클래스의 인스턴스를 제거
		channel.exit(user);
		
		// 참가자 대기실 퇴장
		// - 웹소켓 연결이 해제된 회원의 User 클래스의 인스턴스를 Room 클래스의 방 참가자 저장소인 Set<User> users에 제거
		waitingRoom.leave(user);
		
		// 사용자 퇴장 확인을 위한 로그 출력
		log.debug("사용자 퇴장");
	}

	// 웹소켓 서버에 메시지 전송시
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		
		// WebSocketSession에 저장된 값을 Map 형태로 반환 
		// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
		Map<String, Object> attr = session.getAttributes();
		
		// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
		User user = User.builder()
					.memberId((String)attr.get("loginId"))
					.memberNick((String)attr.get("loginNick"))
					.memberGrade((String)attr.get("loginAuth"))
					.session(session)
				.build();

		// ObjectMapper의 인스턴스 생성 
		ObjectMapper mapper = new ObjectMapper();
		
		// JSON을 VO 형태로 변환
		ReceiveVO receiveVO = mapper.readValue(message.getPayload(), ReceiveVO.class);
		
		// 변환된 VO의 형태를 확인하기 위한 로그 출력
		log.debug("receiveVO = {}", receiveVO);
		
		// 사용자가 특정 방에 참가하는 것인지, 특정 방에 메시지를 보내는 것인지 판정
		if(receiveVO.getType() == 1) { // VO의 type 필드의 값이 1인 경우 (방 참가)
			
			// 대기실의 방 참가자 저장소에서 해당 사용자의 User 클래스의 인스턴스 제거
			waitingRoom.leave(user);
			
			// 참가하려는 방의 방 참가자 저장소에 해당 사용자의 User 클래스의 인스턴스 추가
			channel.join(user, receiveVO.getRoom());
			
			// 해당 사용자의 특정 방 참가를 확인하기 위한 로그 출력
			log.debug("{} 방에 {} 입장", receiveVO.getRoom(), user.getMemberId());
		}
		else if(receiveVO.getType() == 2) { // VO의 type 필드의 값이 2인 경우 (메시지 전송)
			// 메시지 출력 형식에 맞도록 메시지에 필요한 정보 추가
			MessageVO vo = MessageVO.builder()
							.id(user.getMemberId()) // 전송자 아이디
							.nickname(user.getMemberNick()) // 전송자 닉네임
							.auth(user.getMemberGrade()) // 전송자 회원 등급
							.text(receiveVO.getText()) // 전송자 메시지 내용
							.time(new Date()) // 전송 일자
						.build();
			// VO를 JSON 형태의 문자열로 변환
			String json = mapper.writeValueAsString(vo);
			
			// JSON 형태의 문자열로 메시지 생성
			TextMessage msg = new TextMessage(json);
			
			// 해당 사용자가 참가하고있는 방의 이름을 찾아 해당 방에만 메시지 전송
			channel.send(user, msg);
		}
	}
}

 

ReceiveVO

- 웹소켓 서버로 보내는 메시지의 유형 판정을 위한 VO

- 방 입장을 위한 메시지일 경우 type, room 필드의 값이 존재하며 text는 null

- 메시지 전송을 위한 메시지일 경우 type, text 필드의 값이 존재하며 room은 null

@JsonIgnoreProperties // 필드의 값이 모두 있지 않아도 처리가 가능하도록
@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class ReceiveVO {
	
	// 필드
	private int type; // 유형 - 방 참가 / 메시지 전송을 구별하기 위함
	private String room; // 방 이름 - 방 참가를 위한 필드
	private String text; // 메시지 내용 - 메시지 전송을 위한 필드
}

 

WebSocketServerConfiguration

@Configuration
@EnableWebSocket // 웹소켓 활성화
public class WebSocketServerConfiguration implements WebSocketConfigurer {

	// 의존성 주입
	@Autowired
	private MemberGroupChatServer memberGroupChatServer;
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

		// HttpSessionHandshakeInterceptor는 HttpSession을 WebSocketSession으로 넘겨준다
		registry..addHandler(memberGroupChatServer, "/ws/group")
				.addInterceptors(new HttpSessionHandshakeInterceptor())
				.withSockJS();
	}	
}

 

PageController

@Controller
@RequestMapping("/page")
public class PageController {
	
	@GetMapping("/group/{room}")
	public String group(@PathVariable String room, Model model) {
		model.addAttribute("room", room);
		return "group";
	}
}

 

group.jsp

- 웹소켓 연결이 생성되면 웹소켓 서버로 방 입장을 위한 메시지 전송

- 사용자가 입력창에 입력을 한 후 전송 버튼을 누르면 웹소켓 서버로 메시지 전송을 위한 메시지를 전송

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<style>
	.chat-message {
		padding: 1em;
		border: 1px solid black;
		border-radius : 1em;
	}
</style>

<h1>그룹 채팅 예제</h1>
<h2>방 이름 : ${room} </h2>

<hr>

<input type="text" id="message-input">
<button type="button" id="message-send">전송</button>
<hr>
<div id="message-list"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
	$(function(){
		
		//1. 페이지에 연결되면 바로 웹소켓 연결 생성
		// 웹소켓 연결 주소 설정
		var uri = "${pageContext.request.contextPath}/ws/group";
		
		// 웹소켓 연결 생성
		// - 접속시 WebSocket이 아닌 SockJS 객체를 생성
		socket = new SockJS(uri);
		
		// 웹소켓에 대한 이벤트 설정
		// 1) 웹소켓 연결이 생성될 때
		socket.onopen = function(){
			//console.log("open");
			// 웹소켓에 연결되면 바로 웹소켓 서버로 '방 입장' 메시지 전송
			var data = {
				type : 1,
				room : "${room}"
			}
			socket.send(JSON.stringify(data));
		};
		
		// 2) 웹소켓 연결 중 연결이 해제될 때
		socket.onclose = function(){
			//console.log("close");
		};
		
		// 3) 웹소켓 연결 중 연결에 에러가 발생할 때
		socket.onerror = function(){
			//console.log("error");
		};
		
		// 4) 웹소켓 연결 중 메시지가 수신될 때
		socket.onmessage = function(e){
			// 문자열을 JSON 형태로 수신 - JSON.parse()
			var data = JSON.parse(e.data);
			
			// p 태그 지정 - CSS에서 디자인을 하기 위해 클래스 부여
			var p = $("<p>").addClass("chat-message");
			
			// p 태그 지정 - 닉네임[등급] 형태
			var w = $("<p>").text(data.nickname + "[" + data.auth + "]");
			
			// 수신한 JSON의 time 형태 변경 
			var time = moment(data.time).format("YYYY-MM-DD hh:mm");
			
			// p 태그 지정 - (시간) 형태
			var t = $("<p>").text("("+time+")");
			
			// p 태그 지정 - 메시지 표시
			var c = $("<p>").text(data.text);
			
			// 지정된 p 태그들을 연결
			p.append(w).append(c).append(t);
			
			// 메시지를 표시하는 div 안에 연결된 p 태그 생성
			$("#message-list").append(p);
			
			// 메시지가 발생할 때마다 스크롤 하단으로 이동
			var height = $(document).height();
			$(window).scrollTop(height);
		};
			
		
		// 2.
		$(window).on("beforeunload", function(){
			//웹소켓 연결 종료
			//window.socket.close();
			socket.close();
			
		});
		
		// 3. 전송 버튼을 누를 때 
		// - 웹소켓 서버로 '사용자가 입력한' 메시지 전송
		$("#message-send").click(function(){
			// 입력창에 입력된 값을 지정
			var text = $("#message-input").val();
			
			// 입력창에 입력된 값이 없는 상태에서 전송 버튼을 누르면 return
			if(text.length == 0) return;
			
			// JSON으로 변환해서 전송
			// - JSON.stringify(객체) : 객체를 문자열로
			// - JSON.parse(문자열) : 문자열을 객체로
			
			// 입력값을 JSON 형태로 설정
			var data = {
				type : 2,
				text : text
			};
			
			// 입력창의 값 전송
			socket.send(JSON.stringify(data));
			
			// 전송 후 입력창 초기화(비우기)
			$("#message-input").val("");
		});
	});
</script>

'국비교육 > 국비교육' 카테고리의 다른 글

day78 - 1117  (0) 2022.11.18
day77 - 1116  (0) 2022.11.16
day75 - 1114  (0) 2022.11.14
day73 - 1110  (0) 2022.11.12
day72 - 1109  (0) 2022.11.12