Skip to content

Commit

Permalink
Merge pull request #10 from kjungw1025/feat/chatting
Browse files Browse the repository at this point in the history
feat: WebSocket과 Kafka를 이용한 채팅 기능 임시 구현
  • Loading branch information
kjungw1025 authored Jan 23, 2024
2 parents 4c5330d + 6b26153 commit cde92be
Show file tree
Hide file tree
Showing 128 changed files with 62,053 additions and 3 deletions.
15 changes: 15 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ dependencies {
implementation 'com.github.therapi:therapi-runtime-javadoc:0.15.0'
implementation 'org.springdoc:springdoc-openapi-ui:1.6.14'

// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// sockjs
implementation 'org.webjars:sockjs-client:1.5.1'

// stomp
implementation 'org.webjars:stomp-websocket:2.3.4'

// kafka
implementation 'org.springframework.kafka:spring-kafka'

// gson
implementation 'com.google.code.gson:gson:2.9.0'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.dku.council.domain.chat.controller;

import com.dku.council.domain.chat.model.MessageType;
import com.dku.council.domain.chat.model.dto.Message;
import com.dku.council.domain.chat.model.dto.request.RequestChatDto;
import com.dku.council.domain.chat.model.dto.response.ResponseChatDto;
import com.dku.council.domain.chat.service.ChatService;
import com.dku.council.domain.chat.service.MessageSender;
import com.dku.council.global.auth.role.UserAuth;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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.ResponseBody;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

import java.util.List;

@Tag(name = "채팅", description = "채팅 송/수신 관련 api")
@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {

@Value("${spring.kafka.consumer.topic}")
private String topic;

/**
* 아래에서 사용되는 convertAndSend 를 사용하기 위한 선언
* convertAndSend 는 객체를 인자로 넘겨주면 자동으로 Message 객체로 변환 후 도착지로 전송한다.
*/
private final SimpMessageSendingOperations template;

private final ChatService chatService;
private final MessageSender sender;

/**
* 채팅방 별, 입장 이벤트 발생시 처리되는 기능
*
* @param chat 채팅으로 보낼 메시지 정보 관련 param
*
* MessageMapping 을 통해 webSocket 로 들어오는 메시지를 발신 처리한다.
* 이때 클라이언트에서는 /pub/chat/sendMessage 로 요청하게 되고 이것을 controller 가 받아서 처리한다.
* 처리가 완료되면 /sub/chatRoom/enter/roomId 로 메시지가 전송된다.
*/
@MessageMapping("/chat/enterUser")
public void enterUser(@Payload RequestChatDto chat,
SimpMessageHeaderAccessor headerAccessor) {
// 채팅방 유저+1
chatService.plusUserCnt(chat.getRoomId());

// 채팅방에 유저 추가 및 UserUUID 반환
String username = chatService.addUser(chat.getRoomId(), chat.getSender());
log.info("enterUser에서 uuid " + username);
log.info("enterUser에서 roomId " + chat.getRoomId());

// 반환 결과를 socket session 에 userUUID 로 저장
headerAccessor.getSessionAttributes().put("username", username);
headerAccessor.getSessionAttributes().put("roomId", chat.getRoomId());

Message message = Message.builder()
.type(chat.getType())
.roomId(chat.getRoomId())
.sender(chat.getSender())
.message(chat.getSender() + " 님 입장!!")
.build();

sender.send(topic, message);
}

/**
* 채팅방 별, 채팅 메시지 전송 기능
*
* @param chat 채팅으로 보낼 메시지 정보 관련 param
*/
@MessageMapping("/chat/sendMessage")
public void sendMessage(@Payload RequestChatDto chat) {
log.info("CHAT {}", chat);

Message message = Message.builder()
.type(chat.getType())
.roomId(chat.getRoomId())
.sender(chat.getSender())
.message(chat.getMessage())
.build();

sender.send(topic, message);
}

/**
* 채팅방 별, 퇴장 이벤트 발생시 처리되는 기능
*
* 유저 퇴장 시에는 EventListener 을 통해서 유저 퇴장을 확인
*/
@EventListener
public void webSocketDisconnectListener(SessionDisconnectEvent event) {
log.info("DisConnEvent {}", event);

StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

// stomp 세션에 있던 username과 roomId 를 확인해서 채팅방 유저 리스트와 room 에서 해당 유저를 삭제
String username = (String) headerAccessor.getSessionAttributes().get("username");
String roomId = (String) headerAccessor.getSessionAttributes().get("roomId");
log.info("퇴장 controller에서 uuid " + username);
log.info("퇴장 controller에서 roomId " + roomId);

log.info("headAccessor {}", headerAccessor);

// 채팅방 유저 -1
chatService.minusUserCnt(roomId);

// 채팅방 유저 리스트에서 유저 삭제
chatService.delUser(roomId, username);

if (username != null) {
log.info("User Disconnected : ", username);

// builder 어노테이션 활용
Message message = Message.builder()
.type(MessageType.LEAVE)
.sender(username)
.roomId(roomId)
.message(username + " 님 퇴장!!")
.build();

sender.send(topic, message);
}
}

/**
* 채팅방 별, 채팅에 참여한 유저 리스트 반환
*
* @param roomId 채팅방 id
*/
@GetMapping("/chat/userlist")
@UserAuth
@ResponseBody
public List<String> userList(String roomId) {
return chatService.getUserList(roomId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.dku.council.domain.chat.controller;

import com.dku.council.domain.chat.model.dto.response.ResponseChatRoomDto;
import com.dku.council.domain.chat.service.ChatService;
import com.dku.council.domain.user.model.dto.response.ResponseUserInfoForChattingDto;
import com.dku.council.domain.user.service.UserService;
import com.dku.council.global.auth.jwt.AppAuthentication;
import com.dku.council.global.auth.role.UserAuth;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Tag(name = "채팅방", description = "채팅방 관련 api")
@Controller
@RequestMapping("/chatRoom")
@RequiredArgsConstructor
@Slf4j
public class ChatRoomController {
@Autowired
private ChatService chatService;

private final UserService userService;

/**
* 채팅방 리스트 화면
*/
@GetMapping
@UserAuth
public String goChatRoom(Model model, AppAuthentication auth) {
ResponseUserInfoForChattingDto responseUserInfoForChattingDto = userService.getUserInfoForChatting(auth.getUserId());

model.addAttribute("list", chatService.findAllRoom());
model.addAttribute("user", responseUserInfoForChattingDto);

log.info("SHOW ALL ChatList {}", chatService.findAllRoom());

return "/page/chatting/roomlist";
}

/**
* 채팅방 생성
*
* @param name 채팅방 이름
* @param roomPwd 채팅방 비밀번호
* @param secretCheck 채팅방 잠금 설정 여부
* @param maxUserCount 채팅방 최대 인원 수 설정 (default = 10)
*/
@PostMapping("/create")
@UserAuth
public String createRoom(@RequestParam("roomName") String name,
@RequestParam("roomPwd") String roomPwd,
@RequestParam("secretChk") String secretCheck,
@RequestParam(value = "maxUserCount", defaultValue = "10") String maxUserCount,
AppAuthentication auth,
RedirectAttributes rttr) {

ResponseChatRoomDto room = chatService.createChatRoom(name,
roomPwd,
Boolean.parseBoolean(secretCheck),
Integer.parseInt(maxUserCount),
auth.getUserId());

log.info("CREATE Chat Room [{}]", room);

rttr.addFlashAttribute("roomName", room);
return "redirect:/chatRoom";
}


// 채팅방 입장 화면
// 파라미터로 넘어오는 roomId 를 확인후 해당 roomId 를 기준으로
// 채팅방을 찾아서 클라이언트를 chatroom 으로 보낸다.
@GetMapping("/enter")
@UserAuth
public String roomDetail(Model model, String roomId, AppAuthentication auth){

log.info("/chatRoom/enter : roomId {}", roomId);

model.addAttribute("user", userService.getUserInfoForChatting(auth.getUserId()));
model.addAttribute("room", chatService.findRoomById(roomId));

return "/page/chatting/chatroom";
}

/**
* 채팅방 비밀번호 확인
*
* @param roomId 채팅방 id
* @param roomPwd 사용자가 입력한 비밀번호
* @return 사용자가 입력한 비밀번호가 일치하면 true, 아니면 false
*/
@PostMapping("/confirmPwd/{roomId}")
@UserAuth
@ResponseBody
public boolean confirmPwd(@PathVariable String roomId,
@RequestParam String roomPwd){

return chatService.confirmPwd(roomId, roomPwd);
}

/**
* 채팅방 삭제
*
* @param roomId 채팅방 id
*/
@DeleteMapping("/delete/{roomId}")
@UserAuth
public String delChatRoom(@PathVariable String roomId, AppAuthentication auth){

// roomId(UUID 값) 기준으로 채팅방 삭제
chatService.delChatRoom(auth.getUserId(), roomId, auth.isAdmin());

return "redirect:/chatRoom";
}

/**
* maxUserCount에 따른 채팅방 입장 여부
*
* @param roomId 채팅방 Id
* @return true/false
*/
@GetMapping("/chkUserCnt/{roomId}")
@ResponseBody
public boolean chkUserCnt(@PathVariable String roomId){

return chatService.chkRoomUserCnt(roomId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dku.council.domain.chat.exception;

import com.dku.council.global.error.exception.LocalizedMessageException;
import org.springframework.http.HttpStatus;

public class ChatRoomNotFoundException extends LocalizedMessageException {
public ChatRoomNotFoundException() { super(HttpStatus.NOT_FOUND, "notfound.chat-room"); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.dku.council.domain.chat.model;

public enum ChatRoomStatus {
/**
* 활성화 상태
*/
ACTIVE,

/**
* 닫힌 상태
*/
CLOSED,

/**
* 삭제된 상태
*/
DELETED,

/**
* 운영자에 의해 삭제된 상태
*/
DELETED_BY_ADMIN
}
19 changes: 19 additions & 0 deletions src/main/java/com/dku/council/domain/chat/model/MessageType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dku.council.domain.chat.model;

public enum MessageType {
/**
* 채팅방 입장
*/
ENTER,

/**
* 채팅방 대화중
* (해당 채팅방을 sub하고 있는 모든 client들에게 전달됨)
*/
TALK,

/**
* 채팅방 퇴장
*/
LEAVE;
}
Loading

0 comments on commit cde92be

Please sign in to comment.