1. 채팅 기능 1 : 230319
- mariaDB, STOMP, JPA, Mybatis, thymeleaf, ajax
- 실습 코드 :
SpringConfig.java
package com.socketjs.chat.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class SpringConfig implements WebSocketMessageBrokerConfigurer {
// 웹 소켓 연결을 위한 엔드 포인트 설정 및 stomp에서 sub와 pub라고 엔드포인트 설정
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp") // 연결될 엔드포인트
.withSockJS(); // SocketJS를 연결한다는 설정
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메세지를 구독하는 요청 url(= 즉 메세지 받을 때)
registry.enableSimpleBroker("/sub");
// 메세지를 발행하는 요청 url(= 메세지를 보낼 때)
registry.setApplicationDestinationPrefixes("/pub");
}
}
ChatRoomDTO.java
package com.socketjs.chat.dto;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
// 핵심 :
// Stomp를 통해 pub과 sub를 사용하면, 구독자 관리가 알아서 된다!!
// 따라서, 따로 세션 관리를 하는 코드를 작성할 필요도 없고,
// 메세지를 다른 세션의 클라이언트에게 발송하는 것도 구현할 필요가 없다.
@Data
@Builder
@Getter
@Setter
public class ChatRoomDTO {
private String roomId; // 채팅방 아이디
private String roomName; // 채팅방 이름
private int userCount; // 채팅방 인원수
private int maxUserCnt; // 채팅방 최대 인원 수
private String roomPwd; // 채팅방 삭제시 필요할 pwd
private boolean secretChk; // 채팅방 잠금 여부
private HashMap<String, String> userList;
}
ChatDTO.java
package com.socketjs.chat.dto;
import jdk.jfr.DataAmount;
import lombok.*;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class ChatDTO {
// 중요!!
// 메세지 타입을 enum 클래스로 상수 열거형으로 설정!
// 메세지 타입에 따라서 동작하는 구조가 달라진다!!
public enum MessageType{
ENTER, TALK, LEAVE;
}
private MessageType type; // 메세지 타입
private String roomId; // 방 번호
private String sender; // 채팅을 보낸 사람
private String message; // 메세지
private String time; // 채팅 발송 시간
}
ChatRepository.java
package com.socketjs.chat.dao;
import com.socketjs.chat.dto.ChatRoomDTO;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.*;
// 보통 전체 조회, 생성, 수정, 세부 조회, 삭제로 설계한다.
@Repository
@Slf4j
public class ChatRepository {
private Map<String, ChatRoomDTO> chatRoomMap;
// 생성자 1번만 초기화(채팅방 정보와 관련되는 곳이라서 채팅방이 매번 업데이트되면 안되어서)
@PostConstruct
private void init() {
// 그냥 HashMap보다 순서가 정해져 있어서 성능이 약간 더 빠르다.
// 하지만, 티는 잘 안난다.
chatRoomMap = new LinkedHashMap<>();
}
// 채팅방 삭제에 따른 채팅방의 사진 삭제를 위한 fileService 선언
// 아직 생략!
// 전체 채팅방 조회!
// DTO는 대부분 List로 받는다.
public List<ChatRoomDTO> finAllRoom(){
// 채팅방 순서를 최근 순서로 반환
List chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms); // 최근 순이라서 뒤집어서 반환
return chatRooms;
}
// roomId 기준으로 채팅방 찾기!!
// Id만 찾으면 되어서 반환값도 List가 아니라 그냥 일반 DTO 객체의 값 형태로 받는다.
public ChatRoomDTO findRoomById(String roomId) {
return chatRoomMap.get(roomId);
}
// roomName 기준으로 채팅방 만들기
// 추가로 방 비밀번호(방 지울 때 비번), 채팅방 잠글지 여부, 방 참여의 인원수 제한
public ChatRoomDTO createChatRoom(String roomName, String roomPwd, boolean secretChk, int maxUserCnt) {
// @Builder로 생성
ChatRoomDTO chatRoomDTO = ChatRoomDTO.builder()
.roomId(UUID.randomUUID().toString())
.roomName(roomName)
.roomPwd(roomPwd) // 채팅방 지울 때, 패스워드
.secretChk(secretChk) // 채팅방 잠금 여부
.userList(new HashMap<String, String>())
.maxUserCnt(maxUserCnt) // 최대 인원수 제한
.build();
// 위의 build된 정보로 Map 객체의 put 메서드로 채팅방을 진짜로 생성!!
// (key, value 필요)
chatRoomMap.put(chatRoomDTO.getRoomId(), chatRoomDTO);
return chatRoomDTO;
}
// 채팅방 인원 + 1
public void plusUserCnt(String roomId){
ChatRoomDTO room = chatRoomMap.get(roomId);
room.setUserCount(room.getUserCount() + 1);
}
// 채팅방 인원 - 1
public void minusUserCnt(String roomId){
ChatRoomDTO room = chatRoomMap.get(roomId);
room.setUserCount(room.getUserCount() - 1);
}
// maxUserCnt에 따른 채팅방 입장 여부
public boolean chkRoomUserCnt(String roomId){
ChatRoomDTO room = chatRoomMap.get(roomId);
log.info("참여 인원 [{} {}]", room.getUserCount(), room.getMaxUserCnt());
if(room.getUserCount() >= room.getMaxUserCnt()){
return false;
}
return true;
}
// 해당 채팅방에 유저 추가
public String addUser(String roomId, String userName){
ChatRoomDTO room = chatRoomMap.get(roomId);
String userUUID = UUID.randomUUID().toString();
// 아이디 중복 확인 후 userList에 추가
room.getUserList().put(userUUID, userName);
return userUUID;
}
// 채팅방 유저 이름 중복 확인!!
// 사실 이거 필요 없어도 되는데... 아닌가?
public String isDuplicateName(String roomId, String username){
ChatRoomDTO room = chatRoomMap.get(roomId);
String tmp = username;
// 같은 방에 이름이 같은 사람이 있으면 중복 처리를 해주는 로직이다!!
while(room.getUserList().containsValue(tmp)){
int ranNum = (int) (Math.random()*100) + 1; // 1~100 랜덤 숫자
tmp = username + ranNum;
}
return tmp;
}
// 채팅방 유저 리스트 삭제
public void delUser(String roomId, String userUUID){
ChatRoomDTO room = chatRoomMap.get(roomId);
room.getUserList().remove(userUUID);
// 유저 리스트 삭제는 목록이라서 List 객체에서 remove 메서드로 삭제
// 채팅방 삭제는 Map 객체이지만 똑가티 나중에 remove 메서드로 삭제
// 추가로 예외 처리해주기!!(에러 방지)
}
// 채팅방 전체 userName 조회
public String getUserName(String roomId, String userUUID){
ChatRoomDTO room = chatRoomMap.get(roomId);
return room.getUserList().get(userUUID);
}
// 채팅방 전체 userList 조회
public ArrayList<String> getUserList(String roomId){
ArrayList<String> list = new ArrayList<>();
ChatRoomDTO room = chatRoomMap.get(roomId);
// HashMap인 room을 for문을 돌리고 value만 뽑아낸다.
// ArrayList인 list에 add로 저장 후 return
room.getUserList().forEach((key, value)-> list.add(value));
return list;
}
// 채팅방 비밀번호 조회
public boolean confirmPwd(String roomId, String roomPwd) {
return roomPwd.equals(chatRoomMap.get(roomId).getRoomPwd());
}
public void delChatRoom(String roomId){
try {
chatRoomMap.remove(roomId);
// 파일 추가 기능 있으면 파일 삭제도 되어야함..
log.info("삭제 완료 roomId: {}", roomId);
} catch (Exception e){
System.out.println(e.getMessage());
}
}
}
ChatRoomControlller.java
package com.socketjs.chat.controller;
import com.socketjs.chat.dao.ChatRepository;
import com.socketjs.chat.dto.ChatRoomDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
// 컨트롤단이 가장 어렵다!!**
@Controller
@Slf4j
public class ChatRoomController {
@Autowired
private ChatRepository chatRepository;
// 채팅 리스트 화면
// / 로 요청이 들어오면 전체 채팅룸 리스트를 담아서 return
// 스프링 시큐리티는 나중에 구현!!
// 스프링 시큐리티의 로그인 유저 정보는 Security 세션의 PrincipalDetails 안에 담긴다
// 정확히는 PrincipalDetails 안에 ChatUser 객체가 담기고, 이것을 가져오면 된다.
@GetMapping("/")
public String getChatRoom(Model model){
model.addAttribute("list", chatRepository.finAllRoom());
// 스프링 시큐리티는 나중에 구현!!
// if(principalDetails != null){
// model.addAttribute("user",principalDetails.getUser());
// }
log.info("SHOW ALL ChatList {}", chatRepository.finAllRoom());
return "roomlist"; // roomlist.html일 예정?
}
// 채팅방 생성
// 채팅방 생성 후 다시 /로 return
// 인자에 redirectAttr을 넣을 수 있다.(중요!!), addFlashAttribute 공부하기!!
@PostMapping("/chat/createroom")
public String createRoom(@RequestParam("roomName") String name, @RequestParam("roomPwd") String roomPwd, @RequestParam("secretChk") String secretChk,
@RequestParam(value = "maxUserCnt", defaultValue = "100") String maxUserCnt, RedirectAttributes rttr){
ChatRoomDTO room = chatRepository.createChatRoom(name,roomPwd,Boolean.parseBoolean(secretChk), Integer.parseInt(maxUserCnt));
log.info("Create Chat Room [{}]", room);
// addFlashAttribute의 경우에는 flash 속성에 객체를 저장할 수 있다는 것입니다
// 이는 요청 매개 변수(requestparameters)로 값을 전달하지않고 객체로 값을 그대로 전달하게됩니다.
// 그리고 addFlashAttribute는 일회성으로 한번 사용하면 Redirect후 값이 소멸됩니다.
// addAttribute는 값을 지속적으로 사용해야할 때 사용, addFlashAttribute는 일회성으로 사용해야할때 사용해야 합니다.
rttr.addFlashAttribute("roomName",room);
return "redirect:/";
}
// 채팅방 입장 화면
// 파라미터로 넘어오는 roomId를 확인 후 해당 roomId를 기준으로 채팅방을 찾아서
// 클라이언트를 해당 chatroom으로 보낸다.
// 나중에 스프링 시큐리티 추가!**
@GetMapping("/chat/room")
public String roomDetail(Model model, String roomId){
log.info("roomId {}", roomId);
model.addAttribute("room", chatRepository.findRoomById(roomId));
return "chatroom"; // chatroom.html일 예정?
}
// @PathVariable는 보통 비밀번호를 재확인하거나 채팅방을 지울 때, 사용하네..
// Id가 존재하는 것을 DB에서 가져와서 업무에서 확인할 때!!
// 비밀번호를 재확인 요청을 받아서(@PostMapping, @PathVariable 이용)**
@PostMapping("/chat/confirmPwd/{roomId}")
@ResponseBody
public boolean confirmPwd(@PathVariable String roomId, @RequestParam String roomPwd){
// 넘어온 roomID와 roomPwd를 이용하여 비밀번호 찾기
// 찾아서 입력받은 roomPwd와 roomppwd와 비교해서 맞으면 true, 아니면 false
return chatRepository.confirmPwd(roomId, roomPwd);
}
// 채팅방 삭제를 view 단에 출력(@GetMapping, @PathVariable 이용)**
@GetMapping("")
public String delChatRoom(@PathVariable String roomId){
chatRepository.delChatRoom(roomId);
return "redirect:/";
}
// 채팅방 인원수 체크를 view 단에 출력(@GetMapping, @PathVariable 이용)**
@GetMapping("/chat/chkUserCnt/{roomId}")
@ResponseBody
public boolean chkUser(@PathVariable String roomdId){
return chatRepository.chkRoomUserCnt(roomdId);
}
}
ChatControlller.java
package com.socketjs.chat.controller;
import com.socketjs.chat.dao.ChatRepository;
import com.socketjs.chat.dto.ChatDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import java.util.ArrayList;
@Slf4j
@RequiredArgsConstructor
@Controller
public class ChatController {
// 중요!!****
// 아래 코드에서 사용되는 ConvertAndSend를 사용하기 위해서 미리 선언해준다.
// ConvertAndSend는 채팅 관련 객체를 인자로 넘겨주면
// 자동으로 Message 객체로 변환 후 도착지로 전송한다!!!
private final SimpMessageSendingOperations template;
@Autowired
ChatRepository repository;
// MessageMapping을 통해 webSocket로 들어오는 메세지를 발신 처리한다.
// 이때 클라이언트에서는 /pub/chat/message로 요청하게 되고 이것을 controller가 받아서 처리한다.
// 처리가 완료되면 /sub/chat/roomId로 메세지가 전송된다.
// 실시간 해당 유저!!의 정보를 알려준다!!**
@MessageMapping("/chat/enterUser")
public void enterUser(@Payload ChatDTO chat, SimpMessageHeaderAccessor headerAccessor){
// Enter로 들어오면 채팅방의 유저수가 +1 된다.
repository.plusUserCnt(chat.getRoomId());
// 채팅방에 유저 추가 및 UserUUID 반환
String userUUID = repository.addUser(chat.getRoomId(), chat.getSender());
// 반환 결과를 socket session에 userUUID, roomId로 저장해서
// 업무 로직에 사용할 수 있다. 보통 header에 저장!!
headerAccessor.getSessionAttributes().put("userUUID", userUUID);
headerAccessor.getSessionAttributes().put("roomId", chat.getRoomId());
chat.setMessage(chat.getSender() + "님이 입장!!");
template.convertAndSend("/sub/chat/room" + chat.getRoomId(), chat);
}
// MessageMapping을 통해 webSocket로 들어오는 메세지를 발신 처리한다.
// 이때 클라이언트에서는 /pub/chat/message로 요청하게 되고 이것을 controller가 받아서 처리한다.
// 처리가 완료되면 /sub/chat/roomId로 메세지가 전송된다.
// 실시간 실제 채팅 내용이다!!**
@MessageMapping("/chat/sendMessage")
public void sendMessage(@Payload ChatDTO chat){
log.info("CHAT {}", chat);
chat.setMessage(chat.getMessage());
template.convertAndSend("/sub/chat/room/" + chat.getMessage(), chat);
}
// 유저 퇴장 시에 eventListener을 통해서 유저 퇴장을 확인
@EventListener
public void webSocketDisconnectListener(SessionDisconnectEvent event) {
log.info("DisConnEvent {}", event);
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
// Stomp 세션에 저장되어 있던 userUUID, roomID를 확인해서
// 채팅방 유저 리스트와 room에서 해당 유저를 삭제
String userUUID = (String) headerAccessor.getSessionAttributes().get("userUUID");
String roomId = (String) headerAccessor.getSessionAttributes().get("roomID");
log.info("headerAcceser {}", headerAccessor);
// 유저 퇴장 시, 그 유저를 채팅방에서 삭제해서 채팅방 유저수는 -1 된다.
repository.minusUserCnt(roomId);
// 채팅방 유저 리스트에서 UUID 유저 닉네임 조회 및 리스트에서 유저 삭제!
// 전부 삭제해줘야 한다...
String userName = repository.getUserName(roomId, userUUID);
repository.delUser(roomId, userUUID);
if (userName != null) {
log.info("User Disconnected : " + userName);
// builder 어노테이션 활용
ChatDTO chat = ChatDTO.builder()
.type(ChatDTO.MessageType.LEAVE)
.sender(userName)
.message(userName + "님 퇴장!!")
.build();
template.convertAndSend("/sub/chat/room/" + roomId, chat);
}
}
// 채팅에 참여한 유저 리스트 반환
@GetMapping("/chat/userList")
@ResponseBody
public ArrayList<String> userList(String roomId){
return repository.getUserList(roomId);
}
// 채팅에 참여한 유저 닉네임 중복 확인
// username인지 userName인지 구분!! 중복 검사라서!!
@GetMapping("/chat/duplicateName")
@ResponseBody
public String isDuplicateName(@RequestParam("roomId") String roomId, @RequestParam("username") String username) {
String userName = repository.isDuplicateName(roomId, username);
log.info("동작 확인 {}", userName);
return userName;
}
}
chatroom.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<title>My Spring WebSocket Chatting</title>
<link rel="stylesheet" href="/css/chatroom/main.css"/>
<style>
#menu{
width: 310px;
}
button#uploadFile {
margin-left: 225px;
margin-top: -55px;
}
input {
padding-left: 5px;
outline: none;
width: 250px;
margin-top: 15px;
}
.btn fa fa-download {
background-color: DodgerBlue;
border: none;
color: white;
padding: 12px 30px;
cursor: pointer;
font-size: 20px;
}
.input-group{width:80.5%}
.chat-container{position: relative;}
.chat-container .btn-group{position:absolute; bottom:-12px; right:-50px; transform: translate(-50%,-50%);}
</style>
</head>
<body>
<noscript>
<h2>Sorry! Your browser doesn't support Javascript</h2>
</noscript>
<div id="username-page">
<div class="username-page-container">
<h1 class="title">Type your username</h1>
<form id="usernameForm" name="usernameForm">
<div th:if="${user == null}" class="form-group">
<input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control"/>
</div>
<div th:if="${user != null}" class="form-group">
<input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control" th:value="${user.nickName}"/>
</div>
<div class="form-group">
<button type="submit" class="accent username-submit">Start Chatting</button>
</div>
</form>
</div>
</div>
<div id="chat-page" class="hidden">
<div class="btn-group dropend">
<button class="btn btn-secondary dropdown-toggle" type="button" id="showUserListButton" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
참가한 유저
</button>
<div id="list" class="dropdown-menu" aria-labelledby="showUserListButton">
</div>
</div>
<div class="chat-container">
<div class="chat-header">
<h2>[[${room.roomName}]]</h2>
</div>
<div class="connecting">
Connecting...
</div>
<ul id="messageArea">
</ul>
<form id="messageForm" name="messageForm" nameForm="messageForm">
<div class="form-group">
<div class="input-group clearfix">
<input type="text" id="message" placeholder="Type a message..." autocomplete="off"
class="form-control"/>
<button type="submit" class="primary">Send</button>
</div>
</div>
</form>
<div class="btn-group dropend">
<button class="btn btn-secondary dropdown-toggle" type="button" id="showMenu" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
파일 업로드
</button>
<div id="menu" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<input type="file" id="file">
<button type="button" class="btn btn-primary" id="uploadFile" onclick="uploadFile()">저장</button>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/js/chatroom/socket.js"></script>
</body>
</html>
roomlist.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
crossorigin="anonymous"></script>
<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.1.0/css/all.css"
integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
crossorigin="anonymous">
<script src="/js/roomlist/bootstrap-show-password.min.js"></script>
<script th:inline="javascript">
let roomId;
$(function(){
let $maxChk = $("#maxChk");
let $maxUserCnt = $("#maxUserCnt");
// 모달창 열릴 때 이벤트 처리 => roomId 가져오기
$("#enterRoomModal").on("show.bs.modal", function (event) {
roomId = $(event.relatedTarget).data('id');
// console.log("roomId: " + roomId);
});
$("#confirmPwdModal").on("show.bs.modal", function (e) {
roomId = $(e.relatedTarget).data('id');
// console.log("roomId: " + roomId);
});
// 채팅방 설정 시 비밀번호 확인 - keyup 펑션 활용
$("#confirmPwd").on("keyup", function(){
let $confirmPwd = $("#confirmPwd").val();
const $configRoomBtn = $("#configRoomBtn");
let $confirmLabel = $("#confirmLabel");
$.ajax({
type : "post",
url : "/chat/confirmPwd/"+roomId,
data : {
"roomPwd" : $confirmPwd
},
success : function(result){
// console.log("동작완료")
// result 의 결과에 따라서 아래 내용 실행
if(result){ // true 일때는
// $configRoomBtn 를 활성화 상태로 만들고 비밀번호 확인 완료를 출력
$configRoomBtn.attr("class", "btn btn-primary");
$configRoomBtn.attr("aria-disabled", false);
$confirmLabel.html("<span id='confirm'>비밀번호 확인 완료</span>");
$("#confirm").css({
"color" : "#0D6EFD",
"font-weight" : "bold",
});
}else{ // false 일때는
// $configRoomBtn 를 비활성화 상태로 만들고 비밀번호가 틀립니다 문구를 출력
$configRoomBtn.attr("class", "btn btn-primary disabled");
$configRoomBtn.attr("aria-disabled", true);
$confirmLabel.html("<span id='confirm'>비밀번호가 틀립니다</span>");
$("#confirm").css({
"color" : "#FA3E3E",
"font-weight" : "bold",
});
}
}
})
})
// 기본은 유저 설정 칸 미활성화
$maxUserCnt.hide();
// 체크박스 체크에 따라 인원 설정칸 활성화 여부
$maxChk.change(function(){
if($maxChk.is(':checked')){
$maxUserCnt.val('');
$maxUserCnt.show();
}else{
$maxUserCnt.hide();
}
})
})
// 채팅방 생성
function createRoom() {
let name = $("#roomName").val();
let pwd = $("#roomPwd").val();
let secret = $("#secret").is(':checked');
let secretChk = $("#secretChk");
let $maxUserCnt = $("#maxUserCnt");
// console.log("name : " + name);
// console.log("pwd : " + pwd);
if (name === "") {
alert("방 이름은 필수입니다")
return false;
}
if ($("#" + name).length > 0) {
alert("이미 존재하는 방입니다")
return false;
}
if (pwd === "") {
alert("비밀번호는 필수입니다")
return false;
}
// 최소 방 인원 수는 2
if($maxUserCnt.val() <= 1){
alert("혼자서는 채팅이 불가능해요ㅠ.ㅠ");
return false;
}
if (secret) {
secretChk.attr('value', true);
} else {
secretChk.attr('value', false);
}
return true;
}
// 채팅방 입장 시 비밀번호 확인
function enterRoom(){
let $enterPwd = $("#enterPwd").val();
$.ajax({
type : "post",
url : "/chat/confirmPwd/"+roomId,
async : false,
data : {
"roomPwd" : $enterPwd
},
success : function(result){
// console.log("동작완료")
// console.log("확인 : "+chkRoomUserCnt(roomId))
if(result){
if (chkRoomUserCnt(roomId)) {
location.href = "/chat/room?roomId="+roomId;
}
}else{
alert("비밀번호가 틀립니다. \n 비밀번호를 확인해주세요")
}
}
})
}
// 채팅방 삭제
function delRoom(){
location.href = "/chat/delRoom/"+roomId;
}
// 채팅방 입장 시 인원 수에 따라서 입장 여부 결정
function chkRoomUserCnt(roomId){
let chk;
// 비동기 처리 설정 false 로 변경 => ajax 통신이 완료된 후 return 문 실행
// 기본설정 async = true 인 경우에는 ajax 통신 후 결과가 나올 때까지 기다리지 않고 먼저 return 문이 실행되서
// 제대로된 값 - 원하는 값 - 이 return 되지 않아서 문제가 발생한다.
$.ajax({
type : "GET",
url : "/chat/chkUserCnt/"+roomId,
async : false,
success : function(result){
// console.log("여기가 먼저")
if (!result) {
alert("채팅방이 꽉 차서 입장 할 수 없습니다");
}
chk = result;
}
})
return chk;
}
</script>
<style>
a {
text-decoration: none;
}
#table {
margin-top: 40px;
}
h2 {
margin-top: 40px;
}
input#maxUserCnt {
width: 160px;
}
span.input-group-text.input-password-hide {
height: 40px;
}
span.input-group-text.input-password-show {
height: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="container">
<h2>채팅방 리스트</h2>
<div th:if="${user == null}" class="row">
<div class="col">
<a href="/chatlogin"><button type="button" class="btn btn-primary">로그인하기</button></a>
</div>
</div>
<h5 th:if="${user != null}">
[[${user.nickName}]]
</h5>
<table class="table table-hover" id="table">
<tr>
<th scope="col">채팅방명</th>
<th scope="col">잠금 여부</th>
<th scope="col">참여 인원</th>
<th scope="col">채팅방 설정</th>
</tr>
<th:block th:fragment="content">
<tr th:each="room : ${list}">
<span class="hidden" th:id="${room.roomName}"></span>
<td th:if="${room.secretChk}">
<a href="#enterRoomModal" data-bs-toggle="modal" data-target="#enterRoomModal" th:data-id="${room.roomId}">[[${room.roomName}]]</a>
</td>
<td th:if="${!room.secretChk}">
<!-- thymeleaf 의 변수를 onclick 에 넣는 방법 -->
<a th:href="@{/chat/room(roomId=${room.roomId})}" th:roomId="${room.roomId}" onclick="return chkRoomUserCnt(this.getAttribute('roomId'));">[[${room.roomName}]]</a>
</td>
<td>
<span th:if="${room.secretChk}">
🔒︎
</span>
</td>
<td>
<span class="badge bg-primary rounded-pill">[[${room.userCount}]]/[[${room.maxUserCnt}]]</span>
</td>
<td>
<button class="btn btn-primary btn-sm" id="configRoom" data-bs-toggle="modal" data-bs-target="#confirmPwdModal" th:data-id="${room.roomId}">채팅방 설정</button>
</td>
</tr>
</th:block>
</table>
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#roomModal">방 생성</button>
</div>
</div>
<div class="modal fade" id="roomModal" tabindex="-1" aria-labelledby="roomModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">채팅방 생성</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="/chat/createroom" onsubmit="return createRoom()">
<div class="modal-body">
<div class="mb-3">
<label for="roomName" class="col-form-label">방 이름</label>
<input type="text" class="form-control" id="roomName" name="roomName">
</div>
<div class="mb-3">
<label for="roomPwd" class="col-form-label">방 설정 번호(방 삭제시 필요합니다)</label>
<div class="input-group">
<input type="password" name="roomPwd" id="roomPwd" class="form-control" data-toggle="password">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-eye"></i></span>
</div>
</div>
</div>
<div class="mb-3">
<label for="maxUserCnt" class="col-form-label">채팅방 인원 설정(미체크 시 기본 100명)
<input class="form-check-input" type="checkbox" id="maxChk"></label>
<input type="text" class="form-control" id="maxUserCnt" name="maxUserCnt" value="100">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="secret">
<input type="hidden" name="secretChk" id="secretChk" value="">
<label class="form-check-label" for="secret">
채팅방 잠금
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="방 생성하기">
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="enterRoomModal" tabindex="-1" aria-labelledby="enterRoomModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">채팅방 비밀번호를 입력해주세요</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="roomName" class="col-form-label">방 비밀번호</label>
<div class="input-group">
<input type="password" name="roomPwd" id="enterPwd" class="form-control" data-toggle="password">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-eye"></i></span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="enterRoom()">입장하기</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="confirmPwdModal" aria-hidden="true" aria-labelledby="ModalToggleLabel" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">채팅방 설정을 위한 패스워드 확인</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label for="confirmPwd" class="col-form-label" id="confirmLabel">비밀번호 확인</label>
<div class="input-group">
<input type="password" name="confirmPwd" id="confirmPwd" class="form-control" data-toggle="password">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-eye"></i></span>
</div>
</div>
</div>
<div class="modal-footer">
<button id="configRoomBtn" class="btn btn-primary disabled" data-bs-target="#configRoomModal" data-bs-toggle="modal" aria-disabled="true">채팅방 설정하기</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="configRoomModal" tabindex="-1" aria-labelledby="roomModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">채팅방 설정</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="chPwd" class="col-form-label">비밀번호 변경</label>
<div class="input-group">
<input type="password" name="confirmPwd" id="chPwd" class="form-control" data-toggle="password">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-eye"></i></span>
</div>
</div>
</div>
<div class="mb-3">
<label for="chRoomName" class="col-form-label">채팅방 이름 변경</label>
<input type="text" class="form-control" id="chRoomName" name="chRoomName">
</div>
<div class="mb-3">
<label for="chRoomUserCnt" class="col-form-label">채팅방 인원 변경</label>
<input type="text" class="form-control" id="chRoomUserCnt" name="chUserCnt">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="chSecret">
<input type="hidden" name="secretChk" id="chSecretChk" value="">
<label class="form-check-label" for="secret">
채팅방 잠금
</label>
</div>
<div class="mb-3">
<button type="button" class="btn btn-primary" onclick="delRoom()">방 삭제</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>
socket.js
'use strict';
// document.write("<script src='jquery-3.6.1.js'></script>")
document.write("<script\n" +
" src=\"https://code.jquery.com/jquery-3.6.1.min.js\"\n" +
" integrity=\"sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=\"\n" +
" crossorigin=\"anonymous\"></script>")
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
'#2196F3', '#32c787', '#00BCD4', '#ff5652',
'#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
// roomId 파라미터 가져오기
const url = new URL(location.href).searchParams;
const roomId = url.get('roomId');
function connect(event) {
username = document.querySelector('#name').value.trim();
// username 중복 확인
isDuplicateName();
// usernamePage 에 hidden 속성 추가해서 가리고
// chatPage 를 등장시킴
usernamePage.classList.add('hidden');
chatPage.classList.remove('hidden');
// 연결하고자하는 Socket 의 endPoint
var socket = new SockJS('/ws-stomp');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
event.preventDefault();
}
function onConnected() {
// sub 할 url => /sub/chat/room/roomId 로 구독한다
stompClient.subscribe('/sub/chat/room/' + roomId, onMessageReceived);
// 서버에 username 을 가진 유저가 들어왔다는 것을 알림
// /pub/chat/enterUser 로 메시지를 보냄
stompClient.send("/pub/chat/enterUser",
{},
JSON.stringify({
"roomId": roomId,
sender: username,
type: 'ENTER'
})
)
connectingElement.classList.add('hidden');
}
// 유저 닉네임 중복 확인
function isDuplicateName() {
$.ajax({
type: "GET",
url: "/chat/duplicateName",
data: {
"username": username,
"roomId": roomId
},
success: function (data) {
console.log("함수 동작 확인 : " + data);
username = data;
}
})
}
// 유저 리스트 받기
// ajax 로 유저 리스를 받으며 클라이언트가 입장/퇴장 했다는 문구가 나왔을 때마다 실행된다.
function getUserList() {
const $list = $("#list");
$.ajax({
type: "GET",
url: "/chat/userlist",
data: {
"roomId": roomId
},
success: function (data) {
var users = "";
for (let i = 0; i < data.length; i++) {
//console.log("data[i] : "+data[i]);
users += "<li class='dropdown-item'>" + data[i] + "</li>"
}
$list.html(users);
}
})
}
function onError(error) {
connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
connectingElement.style.color = 'red';
}
// 메시지 전송때는 JSON 형식을 메시지를 전달한다.
function sendMessage(event) {
var messageContent = messageInput.value.trim();
if (messageContent && stompClient) {
var chatMessage = {
"roomId": roomId,
sender: username,
message: messageInput.value,
type: 'TALK'
};
stompClient.send("/pub/chat/sendMessage", {}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault();
}
// 메시지를 받을 때도 마찬가지로 JSON 타입으로 받으며,
// 넘어온 JSON 형식의 메시지를 parse 해서 사용한다.
function onMessageReceived(payload) {
//console.log("payload 들어오냐? :"+payload);
var chat = JSON.parse(payload.body);
var messageElement = document.createElement('li');
if (chat.type === 'ENTER') { // chatType 이 enter 라면 아래 내용
messageElement.classList.add('event-message');
chat.content = chat.sender + chat.message;
getUserList();
} else if (chat.type === 'LEAVE') { // chatType 가 leave 라면 아래 내용
messageElement.classList.add('event-message');
chat.content = chat.sender + chat.message;
getUserList();
} else { // chatType 이 talk 라면 아래 내용
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(chat.sender[0]);
avatarElement.appendChild(avatarText);
avatarElement.style['background-color'] = getAvatarColor(chat.sender);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(chat.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var contentElement = document.createElement('p');
// 만약 s3DataUrl 의 값이 null 이 아니라면 => chat 내용이 파일 업로드와 관련된 내용이라면
// img 를 채팅에 보여주는 작업
if(chat.s3DataUrl != null){
var imgElement = document.createElement('img');
imgElement.setAttribute("src", chat.s3DataUrl);
imgElement.setAttribute("width", "300");
imgElement.setAttribute("height", "300");
var downBtnElement = document.createElement('button');
downBtnElement.setAttribute("class", "btn fa fa-download");
downBtnElement.setAttribute("id", "downBtn");
downBtnElement.setAttribute("name", chat.fileName);
downBtnElement.setAttribute("onclick", `downloadFile('${chat.fileName}', '${chat.fileDir}')`);
contentElement.appendChild(imgElement);
contentElement.appendChild(downBtnElement);
}else{
// 만약 s3DataUrl 의 값이 null 이라면
// 이전에 넘어온 채팅 내용 보여주기기
var messageText = document.createTextNode(chat.message);
contentElement.appendChild(messageText);
}
messageElement.appendChild(contentElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
var hash = 0;
for (var i = 0; i < messageSender.length; i++) {
hash = 31 * hash + messageSender.charCodeAt(i);
}
var index = Math.abs(hash % colors.length);
return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)