10.29 추가 : 일반(문자) 채팅만 구현하는 코드는 git 의 master 브렌치를 참고해주시기 바랍니다. master-Webrtc-jpa 는 화상 채팅과 jpa 를 이용한 DB 연결을 포함하는 브렌치입니다.
시작하면서
- 지난번에 S3 파일 업로드 & 다운로드까지 완료하고 나니까 좀 더 여러가지 기능을 넣어보고 싶어서 채팅방에 관한 설정을 추가해보았다.
- 구체적으로는 채팅방 생성 시 비밀번호 설정, 채팅방 인원수 제한과 채팅방 삭제 기능이다. 특히 삭제 기능의 경우 채팅방이 삭제되면 해당 채팅방에 올라온 파일도 필요가 없어짐으로 S3 에서 해당 채팅방에 관련된 모든 파일을 삭제하는 방식으로 코드를 작성하였다.
- 이왕 이렇게 된거 최종적인 완성본은 카카오 오픈 채팅방의 그것처럼 만들어보려고 한다.
- 역시나 이번에도 추가된 모든 내용은 git 에서 확인하실 수 있습니다!
https://github.com/SeJonJ/Spring-WebSocket-Chatting
ChatRoom
- chatRoom 에는 채팅방 인원 제한과 비밀번호, 잠금 설정을 위한 변수를 추가해주었다.
// Stomp 를 통해 pub/sub 를 사용하면 구독자 관리가 알아서 된다!!
// 따라서 따로 세션 관리를 하는 코드를 작성할 필도 없고,
// 메시지를 다른 세션의 클라이언트에게 발송하는 것도 구현 필요가 없다!
@Data
@Builder
public class ChatRoom {
private String roomId; // 채팅방 아이디
private String roomName; // 채팅방 이름
private int userCount; // 채팅방 인원수
private int maxUserCnt; // 채팅방 최대 인원 제한
private String roomPwd; // 채팅방 삭제시 필요한 pwd
private boolean secretChk; // 채팅방 잠금 여부
private HashMap<String, String> userlist;
}
ChatRepository
- 추가된 부분만 정리해보았다.
- chkRoomuserCnt 메서드는 각 chatroom 에 설정된 maxUserCnt 와 chatRoom 의 userCount+1 을 비교해서 - 누군가 입장하는 경우 현재 usercount +1 임으로 - maxUserCnt 보다 userCount+1 이 크다면 false 를 return 하고, 아니면 true 를 return 한다.
- confirmPwd 는 말 그래도 채팅방의 비밀번호를 확인하는 메서드로 매개변수로 받아온 비밀번호와 채팅방의 비밀번호가 일치하는지 여부를 확인하여 그 값을 return 한다.
- delChatRoom 은 채팅방을 삭제하는 메서드이다. 채팅방 삭제는 총 2개의 과정으로 이루어지는데 먼저 ChatRoomMap 에서 삭제하는 작업과 채팅방안에 있는 파일을 삭제 , 즉 해당 채팅방을 기준으로 S3 에 올라간 파일들을 삭제하는 작업이다. S3 쪽은 다시 아래의 S3FileService 를 참고한다.
// 추후 DB 와 연결 시 Service 와 Repository(DAO) 로 분리 예정
@Repository
@Slf4j
public class ChatRepository {
// 채팅방 삭제에 따른 채팅방의 사진 삭제를 위한 fileService 선언
@Autowired
FileService fileService;
// maxUserCnt 에 따른 채팅방 입장 여부
public boolean chkRoomUserCnt(String roomId){
ChatRoom room = chatRoomMap.get(roomId);
log.info("참여인원 확인 [{}, {}]", room.getUserCount(), room.getMaxUserCnt());
if (room.getUserCount() + 1 > room.getMaxUserCnt()) {
return false;
}
return true;
}
// 채팅방 비밀번호 조회
public boolean confirmPwd(String roomId, String roomPwd) {
// String pwd = chatRoomMap.get(roomId).getRoomPwd();
return roomPwd.equals(chatRoomMap.get(roomId).getRoomPwd());
}
// 채팅방 삭제
public void delChatRoom(String roomId) {
try {
// 채팅방 삭제
chatRoomMap.remove(roomId);
// 채팅방 안에 있는 파일 삭제
fileService.deleteFileDir(roomId);
log.info("삭제 완료 roomId : {}", roomId);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
S3FileService - S3 관련 클래스
- 이전에 업로드 & 다운로드 할때 사용했던 클래스 그거 맞다. 다만 이전 글에서는 아래의 deleteFileDir 부분을 사용하지도 않았고, 자세히 설정하지도 않았었다.
- 이 부분은 매개변수로 넘어오는 path 를 경로로 하여 S3 에 있는 파일들 중 해당 경로 - path - 가 포함된 모든 파일들을 삭제해버리는 메서드이다.
- 즉 roomId 가 매개변수로 넘어오는 경우 roomId 가 곧 하나의 경로가 되고, roomId/~~~/~~~ 로 되어있는 모든 파일들을 삭제한다.
// path 아래있는 모든 파일을 삭제한다.
// 이때 path 는 roomId 가 된다 => S3 에 roomId/변경된 파일명(uuid)/원본 파일명 으로 되어있기 때문에
// roomId 를 적어주면 기준이 되는 roomId 아래의 모든 파일이 삭제된다.
@Override
public void deleteFileDir(String path) {
for (S3ObjectSummary summary : amazonS3.listObjects(bucket, path).getObjectSummaries()) {
amazonS3.deleteObject(bucket, summary.getKey());
}
}
html & JavaScript
- 이번에는 html 과 js 도 정리를 해야하는게...이쪽이 정말 많이 변경, 추가 되었다.
- 특히 thymeleaf 로 넘어온 값을 다시 모달창으로 넘기고, 자바스크립트 쪽으로 넘기고 하는 부분을 많이 사용했어서 이 부분들에 대해서 조금이라도 정리하고 넘어가려고 한다.
- 자세한 것은 주석 참고!!
JavaScript
- JS 에서 중요한 부분은 모달창이 열릴때 이벤트 처리하는 부분과 ajax 통신시 동기로 처리하는 부분이다. 내가 만든 채팅은 해당 채팅방을 눌렀을 때 비밀번호를 확인해야하는 경우, 인원 제한을 체크해야하는 경우 등등 많기 때문에 해당 채팅방에 대한 roomId 를 필수적으로 가져와야한다 => 이후 ajax 를 통해 roomId 를 보내서 해당 값들을 확인하기 위해서
- 먼저 모달창으로 값을 넘기는 부분은 $("#모달창id").on("show.bs.modal", function (event) 요런 식으로 작성해야 한다는 것을 기억하자.
- 다음으로 ajax 시 async 를 false 로 설정해둬야 한다. 이는 true 로 설정하면 비동기 통신으로 동작하기 때문에 ajax 요청 처리가 끝나기 전 reutrn chk 가 먼저 나와버리는 경우가 발생하고 이 때문에 chk 에 제대로된 값이 들어가지 않는다. chk에 값이 제대로 들어가지 않을 시 비밀번호 확인이 되지 않거나, 인원수 체크가 제대로 안되거나 등등 문제가 발생했다.
<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.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>
html
- html 에서 중요한 것은 역시나 모달창에 관한 부분이다.
- 정말...정말 정말 어려웠다. 진짜로 js 나 java 쪽보다 시간을 더 썼던 것 같다. 프론트 하시는 분들 정말 위대하다
- 여튼 이쪽의 중요 포인트는 data-bs-target 에 모달의 ID 값을 잘 적어주는 것과 modal 창 안에서도 form 이 가능하다는 정도? => 굉장히 신기했다
- 사실 이 부분들은 bootstrap 공식 문서를 찾아보고, 여기저기 찾아보고 하면서 겨우 작성한 거라서 그냥 이렇게 짰구나...정도만 봐주시면 감사하겠습니다
<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>
<input type="text" class="form-control" id="roomPwd" name="roomPwd">
</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">
</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>
최종 화면
- 채팅방 잠금을 설정하면 해당 비밀번호를 입력해야만 들어갈 수 있다
- 채팅방 인원수를 초과하면 더 이상 들어갈 수 없다
- 채팅방 리스트 화면
- 잠금된 방의 잠금 여부와 참여 인원/제한 인원을 보여준다
- 비밀번호 방을 클릭하면 비밀번호 입력을 위한 모달창이 등장한다
- 방 인원이 꽉 차 있으면 비밀번호를 입력하고 들어가더라도 입장할 수 없다는 alert 창이 등장한다
- 비밀번호 방에 아래와 같이 고양이 사진, 강아지 사진을 업로드하였다 => roomId 를 기억하자
- S3 에 roomId 를 기준으로 2장의 사진이 올라온 것을 확인 할 수 있다
- Reference
https://smujihoon.tistory.com/139
Bootstrap · 세계에서 가장 인기있는 HTML, CSS, JS 라이브러리.
댓글