Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#145, 146 websocket chat #171

Merged
merged 7 commits into from
Feb 26, 2024
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
Loading