Skip to content

Commit

Permalink
Feat/#145, 146 websocket chat (#171)
Browse files Browse the repository at this point in the history
* [FEAT] 채팅 기능 추

* [FEAT] 채팅방 종료시 접근할 수 없도록 로직 추가

* [FEAT] 채팅방 참가 및 퇴장시 로직 추가

* [FEAT] Session 관리를 위한 ConcurrentHashMap Repository 추

* [FEAT] 채팅방 상세 데이터 조회, 채팅 생성 로직 구현

* [FEAT] 채팅방 전체 리스트 조회 구현

* [FEAT] Swagger 추가
  • Loading branch information
tidavid1 authored Feb 26, 2024
1 parent bd83c60 commit 21052b9
Show file tree
Hide file tree
Showing 27 changed files with 739 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.oeid.mogakgo.common.swagger.template;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import io.oeid.mogakgo.core.properties.swagger.error.SwaggerChatErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples;
import io.oeid.mogakgo.domain.chat.application.dto.res.ChatRoomDataRes;
import io.oeid.mogakgo.domain.chat.application.dto.res.ChatRoomPublicRes;
import io.oeid.mogakgo.exception.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.springframework.http.ResponseEntity;

@Tag(name = "Chat", description = "채팅 관련 API")
@SuppressWarnings("unused")
public interface ChatSwagger {

@Operation(summary = "채팅방 목록 조회", description = "사용자의 채팅방 목록을 조회하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "채팅방 목록 조회 성공"),
@ApiResponse(responseCode = "404", description = "요청한 유저가 존재하지 않음",
content = @Content(
mediaType = APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND)
))
})
ResponseEntity<List<ChatRoomPublicRes>> getChatRoomList(@Parameter(hidden = true) Long userId);

@Operation(summary = "채팅방 상세 조회", description = "채팅방의 상세 정보를 조회하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "채팅방 상세 조회 성공"),
@ApiResponse(responseCode = "404", description = "요청한 데이터가 유효하지 않음",
content = @Content(
mediaType = APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND),
@ExampleObject(name = "E030301", value = SwaggerProjectErrorExamples.PROJECT_NOT_FOUND),
@ExampleObject(name = "E110301", value = SwaggerChatErrorExamples.CHAT_ROOM_NOT_FOUND)
}
))
})
ResponseEntity<ChatRoomDataRes> getChatRoomDetailData(@Parameter(hidden = true) Long userId,
@Parameter(in = ParameterIn.PATH) String chatRoomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.oeid.mogakgo.core.configuration;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

private final WebSocketHandler webSocketHandler;

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.oeid.mogakgo.core.properties.swagger.error;

public class SwaggerChatErrorExamples {

public static final String CHAT_ROOM_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E110301\",\"message\":\"해당 채팅방이 존재하지 않습니다.\"}";

private SwaggerChatErrorExamples() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.oeid.mogakgo.domain.chat.application;

import io.oeid.mogakgo.domain.chat.application.dto.req.ChatRoomCreateReq;
import io.oeid.mogakgo.domain.chat.application.dto.res.ChatRoomCreateRes;
import io.oeid.mogakgo.domain.chat.application.dto.res.ChatRoomDataRes;
import io.oeid.mogakgo.domain.chat.application.dto.res.ChatRoomPublicRes;
import io.oeid.mogakgo.domain.chat.entity.document.ChatRoom;
import io.oeid.mogakgo.domain.chat.infrastructure.ChatRepository;
import io.oeid.mogakgo.domain.chat.infrastructure.ChatRoomRoomJpaRepository;
import io.oeid.mogakgo.domain.matching.exception.MatchingException;
import io.oeid.mogakgo.domain.project.domain.entity.Project;
import io.oeid.mogakgo.domain.project.exception.ProjectException;
import io.oeid.mogakgo.domain.project.infrastructure.ProjectJpaRepository;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.domain.user.domain.User;
import io.oeid.mogakgo.exception.code.ErrorCode404;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ChatService {

private final UserCommonService userCommonService;
private final ChatRoomRoomJpaRepository chatRoomRepository;
private final ChatRepository chatRepository;
private final ProjectJpaRepository projectRepository;

// 채팅방 리스트 조회
// TODO 마지막 채팅 기록 가져오기 구현
public List<ChatRoomPublicRes> findAllChatRoomByUserId(Long userId) {
userCommonService.getUserById(userId);
return chatRoomRepository.findAllChatRoomByUserId(userId);
}

// 채팅방 생성
@Transactional
public ChatRoomCreateRes createChatRoom(Long creatorId, ChatRoomCreateReq request) {
Project project = projectRepository.findById(request.getProjectId())
.orElseThrow(() -> new MatchingException(ErrorCode404.PROJECT_NOT_FOUND));
User creator = userCommonService.getUserById(creatorId);
User sender = userCommonService.getUserById(request.getSenderId());
ChatRoom chatRoom = chatRoomRepository.save(
ChatRoom.builder().project(project).creator(creator).sender(sender).build());
chatRepository.createCollection(chatRoom.getId());
return ChatRoomCreateRes.from(chatRoom);
}

// 채팅방 조회
public ChatRoomDataRes findAllChatInChatRoom(Long userId, String chatRoomId) {
var user = userCommonService.getUserById(userId);
var chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new MatchingException(ErrorCode404.CHAT_ROOM_NOT_FOUND));
chatRoom.validateContainsUser(user);
var project = projectRepository.findById(chatRoom.getProject().getId())
.orElseThrow(() -> new ProjectException(ErrorCode404.PROJECT_NOT_FOUND));
var chatList = chatRepository.findAllByCollection(chatRoomId);
return ChatRoomDataRes.of(project.getMeetingInfo(), chatList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.oeid.mogakgo.domain.chat.application;


import static io.oeid.mogakgo.exception.code.ErrorCode500.CHAT_WEB_SOCKET_ERROR;

import io.oeid.mogakgo.domain.chat.entity.document.ChatMessage;
import io.oeid.mogakgo.domain.chat.entity.document.ChatRoom;
import io.oeid.mogakgo.domain.chat.entity.enums.ChatStatus;
import io.oeid.mogakgo.domain.chat.exception.ChatException;
import io.oeid.mogakgo.domain.chat.infrastructure.ChatRepository;
import io.oeid.mogakgo.domain.chat.infrastructure.ChatRoomRoomJpaRepository;
import io.oeid.mogakgo.domain.chat.infrastructure.ChatRoomSessionRepository;
import io.oeid.mogakgo.exception.code.ErrorCode400;
import io.oeid.mogakgo.exception.code.ErrorCode404;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatWebSocketService {

private final ChatRoomRoomJpaRepository chatRoomJpaRepository;
private final ChatRepository chatRepository;
private final ChatRoomSessionRepository chatRoomSessionRepository;

@Transactional(readOnly = true)
public ChatRoom findChatRoomById(String roomId) {
ChatRoom chatRoom = chatRoomJpaRepository.findById(roomId).orElseThrow(() -> new ChatException(ErrorCode404.CHAT_ROOM_NOT_FOUND));
if(chatRoom.getStatus().equals(ChatStatus.CLOSED)){
throw new ChatException(ErrorCode400.CHAT_ROOM_CLOSED);
}
return chatRoom;
}

public void saveChatMessage(ChatMessage chatMessage, String roomId) {
chatRepository.save(chatMessage, roomId);
}

public void closeChatRoom(String roomId) {
ChatRoom chatRoom = chatRoomJpaRepository.findById(roomId).orElseThrow(() -> new ChatException(ErrorCode404.CHAT_ROOM_NOT_FOUND));
chatRoomSessionRepository.removeRoom(chatRoom.getId());
chatRoom.closeChat();
}

public void addSessionToRoom(String roomId, WebSocketSession session) {
chatRoomSessionRepository.addSession(roomId, session);
}

public void removeSessionFromRoom(String roomId, WebSocketSession session) {
chatRoomSessionRepository.removeSession(roomId, session);
}

public void sendMessageToEachSocket(String roomId, TextMessage textMessage){
chatRoomSessionRepository.getSession(roomId).forEach(session -> {
try {
session.sendMessage(textMessage);
} catch (Exception e) {
log.error("sendMessageToEachSocket: {}", e.getMessage());
throw new ChatException(CHAT_WEB_SOCKET_ERROR);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.oeid.mogakgo.domain.chat.application.dto.req;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ChatRoomCreateReq {
private Long projectId;
private Long senderId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.oeid.mogakgo.domain.chat.application.dto.res;

import io.oeid.mogakgo.domain.chat.entity.document.ChatRoom;
import io.oeid.mogakgo.domain.chat.entity.enums.ChatStatus;
import lombok.Getter;

@Getter
public class ChatRoomCreateRes {

private String roomId;
private Long projectId;
private Long creatorId;
private Long senderId;
private ChatStatus chatStatus;

private ChatRoomCreateRes(String roomId, Long projectId, Long creatorId, Long senderId,
ChatStatus chatStatus) {
this.roomId = roomId;
this.projectId = projectId;
this.creatorId = creatorId;
this.senderId = senderId;
this.chatStatus = chatStatus;
}

public static ChatRoomCreateRes from(ChatRoom chatRoom) {
return new ChatRoomCreateRes(chatRoom.getId(), chatRoom.getProject().getId(),
chatRoom.getCreator().getId(), chatRoom.getSender().getId(), chatRoom.getStatus());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.oeid.mogakgo.domain.chat.application.dto.res;

import io.oeid.mogakgo.domain.chat.application.vo.ChatData;
import io.oeid.mogakgo.domain.chat.application.vo.ChatRoomProjectInfo;
import io.oeid.mogakgo.domain.chat.entity.document.ChatMessage;
import io.oeid.mogakgo.domain.project.domain.entity.vo.MeetingInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Schema(description = "채팅방 데이터 조회 응답")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
public class ChatRoomDataRes {

private ChatRoomProjectInfo project;
private List<ChatData> data;

public static ChatRoomDataRes of(MeetingInfo meetingInfo, List<ChatMessage> data) {
ChatRoomProjectInfo project = new ChatRoomProjectInfo(meetingInfo.getMeetDetail(),
meetingInfo.getMeetStartTime(), meetingInfo.getMeetEndTime());
return new ChatRoomDataRes(project, data.stream().map(ChatData::from).toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.oeid.mogakgo.domain.chat.application.dto.res;

import io.oeid.mogakgo.domain.chat.application.vo.ChatUserInfo;
import io.oeid.mogakgo.domain.chat.entity.enums.ChatStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Schema(description = "채팅방 리스트 조회 응답")
@Getter
@AllArgsConstructor
public class ChatRoomPublicRes {
@Schema(description = "프로젝트 ID")
private Long projectId;
@Schema(description = "채팅방 ID")
private String chatRoomId;
@Schema(description = "마지막 메시지")
private String lastMessage;
@Schema(description = "마지막 메시지 생성 시간")
private LocalDateTime lastMessageCreatedAt;
@Schema(description = "채팅방 상태")
private ChatStatus status;

private List<ChatUserInfo> profiles;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.oeid.mogakgo.domain.chat.application.vo;

import io.oeid.mogakgo.domain.chat.entity.document.ChatMessage;
import io.oeid.mogakgo.domain.chat.entity.enums.ChatMessageType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Schema(description = "채팅 데이터")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatData {

@Schema(description = "메시지 타입")
private ChatMessageType messageType;
@Schema(description = "보낸 사람 ID")
private Long senderId;
@Schema(description = "메시지")
private String message;
@Schema(description = "생성 시간")
private LocalDateTime createdAt;

public static ChatData from(ChatMessage chatMessage) {
return new ChatData(chatMessage.getMessageType(), chatMessage.getSenderId(),
chatMessage.getMessage(), chatMessage.getCreatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.oeid.mogakgo.domain.chat.application.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Schema(description = "채팅방 프로젝트 정보")
@Getter
@AllArgsConstructor
public class ChatRoomProjectInfo {

@Schema(description = "프로젝트 설명")
private String meetDetail;
@Schema(description = "프로젝트 시작 시간")
private LocalDateTime meetStartTime;
@Schema(description = "프로젝트 종료 시간")
private LocalDateTime meetEndTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.oeid.mogakgo.domain.chat.application.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Schema(description = "채팅방 유저 정보")
@Getter
@AllArgsConstructor
public class ChatUserInfo {
@Schema(description = "유저 ID")
private Long userId;
@Schema(description = "유저 이름")
private String username;
@Schema(description = "유저 프로필 이미지 URL")
private String avatarUrl;
}
Loading

0 comments on commit 21052b9

Please sign in to comment.