개발 기록
220117 springboot 웹소켓으로 채팅 기능 구현 -1 본문
저번에 웹소켓으로 댓글 알림 후 채팅 기능에도 도전..
채팅방 만들고 입장 퇴장 알림, 대화까지 완성
채팅방 참여인원만 추가하면 됨..
고민 중인 것은.. 입장하고 퇴장할때 count를 늘리고 줄이는 방법은,, 같은 사람이 입장할때마다 숫자가 올라가는 것 같아 entity를 새로 만들어서 저장하고 갯수를 새야하나 고민중.. 새로 만들기는 귀찮은데
내가 이해하려고 쓰는 과정
채팅방이 만들어짐 -> 특정 채팅방에 들어가면 웹소켓과 연결되면서 /enter api에 입장알림 메시지가 보내지면서 입장했습니다 메시지 뜸 (퇴장도 같은 과정) -> 메시지를 보내면 /message api로 메시지가 가는데 현재 접속되어 있는 유저 정보와(카카오아이디 사용) 메시지 전송시 같이 보내진 유저정보가 같을때와 다를때 각각 다른 색의 말풍선 사용
--> 로컬스토리지에 있는 username(=db에 저장되어있는 카카오아이디)를 사용해서 유저정보를 가져오기 위해 카카오아이디로 유저정보를 조회할 수 있는 api를 추가했다 ( ㅠㅠ 더 간단한 방법이 있을 것 같은데 일단 이렇게 함 / 프론트에서 유저정보를 어떻게 가져올지 이것밖에 생각이 안 났음 )
**입장,퇴장 안내 메시지는 공통이므로 클라이언트에서 보내기보다 서버에서 일괄적으로 처리하는게 낫다고 어떤 블로그에서 말했다. 나중에 할 수 있으면 그렇게 리팩토링 해봐야겠다.
서버
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'org.webjars:sockjs-client:1.1.2'
-chatRoom이라는 클래스를 하나 만들어줬다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class ChatRoom extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
public ChatRoom(String name, User user) {
this.name = name;
this.user = user;
}
}
-메세지DTO와 채팅방을 생성하고 조회할때 사용하는 requestDto, responseDto도 각각 만들어줌
(원래는 하나의 dto였는데 반환값이 조금 달라서 그냥 두개로 나눴다)
@Getter
@Setter
public class ChatMessageDto {
private String roomId;
private String sender;
private String message;
}
requestDto는 채팅방 이름만 받으면 된다
@Getter
@Setter
@NoArgsConstructor
public class ChatRoomRequestDto {
private String name;
public ChatRoomRequestDto(String name) {
this.name = name;
}
}
responseDto는 조회할때 필요한 값들 (방번호, 방이름, 생성날짜, 생성자가 필요했음)
@Getter
@Setter
@NoArgsConstructor
public class ChatRoomResponseDto {
private Long roomId;
private String name;
private String createdDate;
private String creator;
public ChatRoomResponseDto(ChatRoom chatRoom) {
this.roomId = chatRoom.getId();
this.name= chatRoom.getName();
this.createdDate = chatRoom.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
this.creator = chatRoom.getUser().getKakaoId();
}
}
Controller
입장,퇴장,메시지 전송시 알림 메시지 내용을 다르게 전송하기 위해서 api도 나눴다. ( 안 나눠도 될 것 같긴 함 )
@Controller
@RequiredArgsConstructor
public class ChatApiController {
private final SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/chat/enter")
public void enter(ChatMessageDto messageDto){
messagingTemplate.convertAndSend("/sub/chat/room/" + messageDto.getRoomId(), messageDto);
}
@MessageMapping("/chat/message")
public void message(ChatMessageDto messageDto) {
messagingTemplate.convertAndSend("/sub/chat/room/" + messageDto.getRoomId(), messageDto);
}
@MessageMapping("/chat/exit")
public void exit(ChatMessageDto messageDto) {
messagingTemplate.convertAndSend("/sub/chat/room/" + messageDto.getRoomId(), messageDto);
}
}
+ 채팅방 생성, 조회 등과 관련된 api 컨트롤러
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatRoomApiController {
private final ChatRoomService chatRoomService;
/**
* 채팅방 목록 조회 API
*/
@GetMapping("/rooms")
public List<ChatRoomResponseDto> getRooms() {
return chatRoomService.getAllRooms();
}
/**
* 채팅방 생성 API
*/
@PostMapping("/room")
public ResultResponseDto createRoom(@RequestBody ChatRoomRequestDto chatRoomDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
chatRoomService.createRoom(chatRoomDto, userDetails.getUser());
return new ResultResponseDto("success", "채팅 방이 생성되었습니다.");
}
/**
* 채팅방 상세 조회 API
*/
@GetMapping("/room/{roomId}")
public ChatRoomResponseDto getRoom(@PathVariable Long roomId) {
return chatRoomService.getRoom(roomId);
}
/**
* 채팅방 삭제 API
*/
@DeleteMapping("/room/{roomId}")
public ResultResponseDto deleteRoom(@PathVariable Long roomId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
chatRoomService.deleteRoom(roomId, userDetails.getUser());
return new ResultResponseDto("success", "채팅 방이 삭제되었습니다.");
}
}
service
서비스도 간단하게 생성, 조회, 삭제와 관련된 로직들
@Slf4j
@RequiredArgsConstructor
@Service
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
@Transactional
public List<ChatRoomResponseDto> getAllRooms() {
return chatRoomRepository.findAll()
.stream()
.map(ChatRoomResponseDto::new)
.collect(Collectors.toList());
}
@Transactional
public void createRoom(ChatRoomRequestDto chatRoomDto, User user) {
chatRoomRepository.save(new ChatRoom(chatRoomDto.getName(), user));
}
@Transactional
public ChatRoomResponseDto getRoom(Long roomId) {
ChatRoom byId = chatRoomRepository.findById(roomId).orElseThrow(
() -> new ApiRequestException("해당 채팅방이 없습니다.")
);
return new ChatRoomResponseDto(byId);
}
@Transactional
public void deleteRoom(Long roomId, User user) {
ChatRoom chatRoom = chatRoomRepository.findByIdAndUser(roomId, user);
chatRoomRepository.delete(chatRoom);
}
}
sebSocketConfig
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub"); // 메모리 기반 메세지 브로커가 해당 api를 구독하고 있는 클라이언트에게 메세지를 전달한다.
registry.setApplicationDestinationPrefixes("/pub"); // 서버에서 클라이언트로부터의 메세지를 받을 api를 prefix 설정한다.
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) { // 클라이언트에서 websoket을 연결할 api를 설정한다.
registry.addEndpoint("/ws-stomp").setAllowedOrigins("*").withSockJS();
}
}
끝인가..?
클라이언트
채팅방 리스트 부분은 생략하고 채팅방에 들어가 웹소켓 연결되는 코드만
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.2/sockjs.min.js"></script>
-채팅방에 들어가면 채팅방 정보 조회 함수와 웹소켓 연결 함수가 실행되도록 한다
let userOwn = localStorage.getItem('username'); //현재 접속하고 있는 유저
//채팅방 정보 가져오기
function getRoomInfo() {
$.ajax({
type: "GET",
url: `/chat/room/${roomId}`,
data: {},
success: function (response) {
console.log(response);
roomId = response['roomId'];
roomName = response['name'];
creator = response['creator'];
$('#room-name').text(roomName);
//채팅방 개설자는 삭제버튼 보이기
if (creator === userOwn) {
$('#chatButton').append(`<button type="button" class="btn btn-danger" style="margin-left: 10px" onclick="deleteRoom()">채팅 방 삭제</button>`)
}
}
});
}
-웹소켓 연결 함수
채팅방에 들어가면 웹소켓 연결이 되고 입장 메시지가 보내지면 같이 전송된 메시지와 보낸 사람 값을 추출해서
또 메시지를 나타내는 함(showMsg)에 보내줬다 ( 다른 사람이 보낸 메시지랑 구별하기 위해서...... )
반환 값을 split으로 나눠서 값을 얻는 건 너무 예뻐보이지 않아서... 다른 방법으로 고쳐야겠음
//웹소켓연결
function onSocket() {
let socket = new SockJS('http://localhost:8080/ws-stomp');
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
stompClient.subscribe('/sub/chat/room/' + roomId, function (chat) {
//메시지 날린사람 정보 가져오기
let kakaoId = chat.body.split('"')[7];
let message = chat.body.split('"')[11];
$.ajax({
type: "GET",
url: `/user/${kakaoId}`,
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('token'));
},
success: function (response) {
username = response['username'];
imgLink = response['imgUrl'];
console.log(response)
//보낸 메시지 나타내기
showMsg(response, kakaoId, message);
}
})
});
//입장 알림 메시지 보내기
stompClient.send("/pub/chat/enter", {}, JSON.stringify({
'roomId': roomId, 'sender': userOwn, 'message': "입장했습니다."
}))
});
}
-메세지를 작성하고 전송을 눌렀을때 실행되는 함수
//채팅 전송
function msgSend() {
let msg = $("#chatMsg").val();
console.log(msg)
if (msg !== "") {
stompClient.send("/pub/chat/message", {}, JSON.stringify({
'roomId': roomId,
'sender': userOwn,
'message': msg
}));
$("#chatMsg").val("");
}
}
-채팅방 나가기
//채팅방 나가기
function exitRoom() {
stompClient.send("/pub/chat/exit", {}, JSON.stringify({
'roomId': roomId,
'sender': userOwn,
'message': "퇴장했습니다."
}));
if (stompClient !== null) {
stompClient.disconnect();
}
window.location.href = "chat.html"
}
비루한 결과물 ㅎㅎ
말풍선이 조절되고 그런건.. 다음에.. 참여인원도 다음에...
'TIL' 카테고리의 다른 글
220120 springboot 웹소켓으로 채팅 기능 구현 -2 (참여인원 표시) (0) | 2022.01.20 |
---|---|
220119 카카오 토큰 갱신?! (0) | 2022.01.19 |
220116 DTO 생성자 (완전 멍청 실수) (0) | 2022.01.16 |
220108 TIL (2) | 2022.01.08 |
Server-Sent Events를 이용한 실시간 댓글 알림 (0) | 2022.01.07 |