diff --git a/be/build.gradle b/be/build.gradle index c0d792e5d..ea53354b0 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -66,6 +66,10 @@ dependencies { //redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // webSocket + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.webjars:stomp-websocket' } tasks.named('test') { diff --git a/be/src/main/java/kr/codesquad/chat/controller/ChatController.java b/be/src/main/java/kr/codesquad/chat/controller/ChatController.java new file mode 100644 index 000000000..9e1cc5aba --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/controller/ChatController.java @@ -0,0 +1,30 @@ +package kr.codesquad.chat.controller; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import kr.codesquad.chat.dto.ChatRoomCreateRequest; +import kr.codesquad.chat.service.ChatService; +import kr.codesquad.util.Constants; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +public class ChatController { + + private final ChatService chatService; + + @PostMapping("/") + public ResponseEntity createChatRoom(@RequestBody ChatRoomCreateRequest chatRoomCreateRequest, + HttpServletRequest request) { + String loginId = (String)request.getAttribute(Constants.LOGIN_ID); + chatService.createRoom(chatRoomCreateRequest, loginId); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/ChatMapper.java b/be/src/main/java/kr/codesquad/chat/dto/ChatMapper.java new file mode 100644 index 000000000..1dcef2bde --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/ChatMapper.java @@ -0,0 +1,15 @@ +package kr.codesquad.chat.dto; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import kr.codesquad.chat.entity.ChatRoom; + +@Mapper +public interface ChatMapper { + ChatMapper INSTANCE = Mappers.getMapper(ChatMapper.class); + + @Mapping(target = "senderId", source = "userId") + ChatRoom toChatRoom(ChatRoomCreateRequest chatRoomCreateRequest, Long userId); +} diff --git a/be/src/main/java/kr/codesquad/chat/dto/ChatRoomCreateRequest.java b/be/src/main/java/kr/codesquad/chat/dto/ChatRoomCreateRequest.java new file mode 100644 index 000000000..1754cddaf --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/dto/ChatRoomCreateRequest.java @@ -0,0 +1,8 @@ +package kr.codesquad.chat.dto; + +import lombok.Getter; + +@Getter +public class ChatRoomCreateRequest { + private Long itemId; +} diff --git a/be/src/main/java/kr/codesquad/chat/entity/Message.java b/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java similarity index 77% rename from be/src/main/java/kr/codesquad/chat/entity/Message.java rename to be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java index 7c0ac5fdd..28b6066ca 100644 --- a/be/src/main/java/kr/codesquad/chat/entity/Message.java +++ b/be/src/main/java/kr/codesquad/chat/entity/ChatMessage.java @@ -14,19 +14,19 @@ @Getter @Entity @NoArgsConstructor -public class Message extends TimeStamped { +public class ChatMessage extends TimeStamped { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) - private Long chatId; + private Long chatRoomId; @Column(nullable = false, length = 200) private String content; @Builder - public Message(Long id, Long chatId, String content) { + public ChatMessage(Long id, Long chatRoomId, String content) { this.id = id; - this.chatId = chatId; + this.chatRoomId = chatRoomId; this.content = content; } } diff --git a/be/src/main/java/kr/codesquad/chat/entity/Chat.java b/be/src/main/java/kr/codesquad/chat/entity/ChatRoom.java similarity index 83% rename from be/src/main/java/kr/codesquad/chat/entity/Chat.java rename to be/src/main/java/kr/codesquad/chat/entity/ChatRoom.java index f0fa67bdd..7cad68549 100644 --- a/be/src/main/java/kr/codesquad/chat/entity/Chat.java +++ b/be/src/main/java/kr/codesquad/chat/entity/ChatRoom.java @@ -13,19 +13,19 @@ @Getter @Entity @NoArgsConstructor -public class Chat { +public class ChatRoom { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private Long itemId; - @Column(nullable = false, length = 45) + @Column(length = 45) private String lastMessageId; @Column(nullable = false) private Long senderId; @Builder - public Chat(Long id, Long itemId, String lastMessageId, Long senderId) { + public ChatRoom(Long id, Long itemId, String lastMessageId, Long senderId) { this.id = id; this.itemId = itemId; this.lastMessageId = lastMessageId; diff --git a/be/src/main/java/kr/codesquad/chat/repository/ChatMessageRepository.java b/be/src/main/java/kr/codesquad/chat/repository/ChatMessageRepository.java new file mode 100644 index 000000000..5b52c3967 --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/repository/ChatMessageRepository.java @@ -0,0 +1,9 @@ +package kr.codesquad.chat.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import kr.codesquad.chat.entity.ChatMessage; + +public interface ChatMessageRepository extends JpaRepository { + +} diff --git a/be/src/main/java/kr/codesquad/chat/repository/ChatRepository.java b/be/src/main/java/kr/codesquad/chat/repository/ChatRoomRepository.java similarity index 53% rename from be/src/main/java/kr/codesquad/chat/repository/ChatRepository.java rename to be/src/main/java/kr/codesquad/chat/repository/ChatRoomRepository.java index 749aecceb..2843a7428 100644 --- a/be/src/main/java/kr/codesquad/chat/repository/ChatRepository.java +++ b/be/src/main/java/kr/codesquad/chat/repository/ChatRoomRepository.java @@ -2,8 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; -import kr.codesquad.chat.entity.Chat; +import kr.codesquad.chat.entity.ChatRoom; -public interface ChatRepository extends JpaRepository { +public interface ChatRoomRepository extends JpaRepository { int countByItemId(Long itemId); } diff --git a/be/src/main/java/kr/codesquad/chat/repository/MessageRepository.java b/be/src/main/java/kr/codesquad/chat/repository/MessageRepository.java deleted file mode 100644 index 2ebadb6ea..000000000 --- a/be/src/main/java/kr/codesquad/chat/repository/MessageRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package kr.codesquad.chat.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import kr.codesquad.chat.entity.Message; - -public interface MessageRepository extends JpaRepository { - -} diff --git a/be/src/main/java/kr/codesquad/chat/service/ChatService.java b/be/src/main/java/kr/codesquad/chat/service/ChatService.java new file mode 100644 index 000000000..2e5ca2eaf --- /dev/null +++ b/be/src/main/java/kr/codesquad/chat/service/ChatService.java @@ -0,0 +1,26 @@ +package kr.codesquad.chat.service; + +import org.springframework.stereotype.Service; + +import kr.codesquad.chat.dto.ChatMapper; +import kr.codesquad.chat.dto.ChatRoomCreateRequest; +import kr.codesquad.chat.repository.ChatMessageRepository; +import kr.codesquad.chat.repository.ChatRoomRepository; +import kr.codesquad.user.entity.User; +import kr.codesquad.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class ChatService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final UserRepository userRepository; + + public void createRoom(ChatRoomCreateRequest chatRoomCreateRequest, String loginId) { + User user = userRepository.findByLoginId(loginId); + chatRoomRepository.save(ChatMapper.INSTANCE.toChatRoom(chatRoomCreateRequest, user.getId())); + } + +} diff --git a/be/src/main/java/kr/codesquad/core/config/SecurityConfig.java b/be/src/main/java/kr/codesquad/core/config/SecurityConfig.java index 8c52003ee..bf5032a96 100644 --- a/be/src/main/java/kr/codesquad/core/config/SecurityConfig.java +++ b/be/src/main/java/kr/codesquad/core/config/SecurityConfig.java @@ -64,7 +64,7 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() - .antMatchers("/h2-console/**", "/api/locations**").permitAll() + .antMatchers("/h2-console/**", "/api/locations**", "/ws/chat").permitAll() .antMatchers(HttpMethod.POST, "/api/users").permitAll() .anyRequest() .authenticated() //다른 요청은 인증 필요함 diff --git a/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java b/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java new file mode 100644 index 000000000..584469a25 --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java @@ -0,0 +1,23 @@ +package kr.codesquad.core.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +import kr.codesquad.core.websocket.WebSocketHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@RequiredArgsConstructor +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + private final WebSocketHandler webSocketHandler; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*"); + } +} diff --git a/be/src/main/java/kr/codesquad/core/filter/AuthorizationFilter.java b/be/src/main/java/kr/codesquad/core/filter/AuthorizationFilter.java index 90f04df69..45c10e5b8 100644 --- a/be/src/main/java/kr/codesquad/core/filter/AuthorizationFilter.java +++ b/be/src/main/java/kr/codesquad/core/filter/AuthorizationFilter.java @@ -26,7 +26,8 @@ @RequiredArgsConstructor public class AuthorizationFilter implements Filter { private static final String[] whiteListUris = {"/h2-console/**", "/api/users", "/api/login", - "/api/reissue-access-token", "/api/oauth/**", "/api/redirect/**", "/redirect/**", "/api/locations**"}; + "/api/reissue-access-token", "/api/oauth/**", "/api/redirect/**", "/redirect/**", "/api/locations**", + "/ws/chat"}; private final JwtProvider jwtProvider; private final RedisUtil redisUtil; diff --git a/be/src/main/java/kr/codesquad/core/websocket/Message.java b/be/src/main/java/kr/codesquad/core/websocket/Message.java new file mode 100644 index 000000000..58061a5de --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/websocket/Message.java @@ -0,0 +1,12 @@ +package kr.codesquad.core.websocket; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Message { + private String roomId; + private String sender; + private String message; +} diff --git a/be/src/main/java/kr/codesquad/core/websocket/MsgController.java b/be/src/main/java/kr/codesquad/core/websocket/MsgController.java new file mode 100644 index 000000000..df774b3ed --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/websocket/MsgController.java @@ -0,0 +1,29 @@ +package kr.codesquad.core.websocket; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chat") +public class MsgController { + private final MsgService msgService; + + @PostMapping + public MsgRoom createRoom(@RequestParam String name) { + return msgService.createRoom(name); + } + + @GetMapping + public List findAllRoom() { + return msgService.findAllRoom(); + } + +} diff --git a/be/src/main/java/kr/codesquad/core/websocket/MsgRoom.java b/be/src/main/java/kr/codesquad/core/websocket/MsgRoom.java new file mode 100644 index 000000000..36c9ee887 --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/websocket/MsgRoom.java @@ -0,0 +1,33 @@ +package kr.codesquad.core.websocket; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.web.socket.WebSocketSession; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class MsgRoom { + private String roomId; + private Set sessions = new HashSet<>(); + + @Builder + public MsgRoom(String roomId) { + this.roomId = roomId; + } + + public void handleActions(WebSocketSession session, Message message, MsgService msgService) { + sessions.add(session); + sendMessage(message, msgService); + } + + public void sendMessage(T message, MsgService messageService) { + sessions.parallelStream().forEach(session -> messageService.sendMessage(session, message)); + } + +} + diff --git a/be/src/main/java/kr/codesquad/core/websocket/MsgService.java b/be/src/main/java/kr/codesquad/core/websocket/MsgService.java new file mode 100644 index 000000000..79a202f34 --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/websocket/MsgService.java @@ -0,0 +1,52 @@ +package kr.codesquad.core.websocket; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.springframework.stereotype.Service; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MsgService { + private final ObjectMapper objectMapper; + private Map msgRooms; + + @PostConstruct + private void init() { + msgRooms = new LinkedHashMap<>(); + } + + public List findAllRoom() { + return new ArrayList<>(msgRooms.values()); + } + + public MsgRoom findById(String roomId) { + return msgRooms.get(roomId); + } + + public MsgRoom createRoom(String name) { + String roomId = name; + return MsgRoom.builder().roomId(roomId).build(); + } + + public void sendMessage(WebSocketSession session, T message) { + try { + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message))); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/be/src/main/java/kr/codesquad/core/websocket/WebSocketHandler.java b/be/src/main/java/kr/codesquad/core/websocket/WebSocketHandler.java new file mode 100644 index 000000000..605419f94 --- /dev/null +++ b/be/src/main/java/kr/codesquad/core/websocket/WebSocketHandler.java @@ -0,0 +1,30 @@ +package kr.codesquad.core.websocket; + +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class WebSocketHandler extends TextWebSocketHandler { + + private final MsgService msgService; + private final ObjectMapper objectMapper; + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + log.info("payload:{}", payload); + + Message msg = objectMapper.readValue(payload, Message.class); + MsgRoom room = msgService.findById(msg.getRoomId()); + room.handleActions(session, msg, msgService); + } +} diff --git a/be/src/main/java/kr/codesquad/item/service/ItemService.java b/be/src/main/java/kr/codesquad/item/service/ItemService.java index 99c978560..f984b8456 100644 --- a/be/src/main/java/kr/codesquad/item/service/ItemService.java +++ b/be/src/main/java/kr/codesquad/item/service/ItemService.java @@ -3,19 +3,19 @@ import java.util.List; import java.util.stream.Collectors; -import kr.codesquad.item.dto.request.ItemStatusDto; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import kr.codesquad.category.repository.CategoryRepository; -import kr.codesquad.chat.repository.ChatRepository; +import kr.codesquad.chat.repository.ChatRoomRepository; import kr.codesquad.favorite.repository.FavoriteRepository; import kr.codesquad.image.entity.Image; import kr.codesquad.image.repository.ImageRepository; import kr.codesquad.image.service.AmazonS3Service; import kr.codesquad.item.dto.ItemMapper; import kr.codesquad.item.dto.request.ItemSaveRequest; +import kr.codesquad.item.dto.request.ItemStatusDto; import kr.codesquad.item.dto.request.ItemUpdateRequest; import kr.codesquad.item.dto.response.ItemCountDataResponse; import kr.codesquad.item.dto.response.ItemDetailResponse; @@ -47,7 +47,7 @@ public class ItemService { private final CategoryRepository categoryRepository; private final FavoriteRepository favoriteRepository; private final LocationRepository locationRepository; - private final ChatRepository chatRepository; + private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; private final ItemPaginationRepository itemPaginationRepository; private final AmazonS3Service amazonS3Service; @@ -97,7 +97,7 @@ public ItemDetailResponse getItem(Long id, String userLoginId) { List images = imageRepository.findByItemId(item.getId()); String categoryName = categoryRepository.findNameById(item.getCategoryId()); - int chatCount = chatRepository.countByItemId(item.getId()); + int chatCount = chatRoomRepository.countByItemId(item.getId()); int favoriteCount = favoriteRepository.countByItemId(item.getId()); return ItemDetailResponse.builder() diff --git a/be/src/test/java/kr/codesquad/service/ItemServiceTest.java b/be/src/test/java/kr/codesquad/service/ItemServiceTest.java index 752fdcd2c..cf7e68ba5 100644 --- a/be/src/test/java/kr/codesquad/service/ItemServiceTest.java +++ b/be/src/test/java/kr/codesquad/service/ItemServiceTest.java @@ -13,10 +13,10 @@ import kr.codesquad.IntegrationTestSupport; import kr.codesquad.category.entity.Category; import kr.codesquad.category.repository.CategoryRepository; -import kr.codesquad.chat.entity.Chat; -import kr.codesquad.chat.entity.Message; -import kr.codesquad.chat.repository.ChatRepository; -import kr.codesquad.chat.repository.MessageRepository; +import kr.codesquad.chat.entity.ChatMessage; +import kr.codesquad.chat.entity.ChatRoom; +import kr.codesquad.chat.repository.ChatMessageRepository; +import kr.codesquad.chat.repository.ChatRoomRepository; import kr.codesquad.favorite.entity.Favorite; import kr.codesquad.favorite.repository.FavoriteRepository; import kr.codesquad.item.dto.slice.ItemListSlice; @@ -45,19 +45,19 @@ public class ItemServiceTest extends IntegrationTestSupport { @Autowired CategoryRepository categoryRepository; @Autowired - ChatRepository chatRepository; + ChatRoomRepository chatRoomRepository; @Autowired FavoriteRepository favoriteRepository; @Autowired - MessageRepository messageRepository; + ChatMessageRepository chatMessageRepository; @AfterEach void dbClean() { userRepository.deleteAllInBatch(); locationRepository.deleteAllInBatch(); - chatRepository.deleteAllInBatch(); + chatRoomRepository.deleteAllInBatch(); favoriteRepository.deleteAllInBatch(); - messageRepository.deleteAllInBatch(); + chatMessageRepository.deleteAllInBatch(); itemRepository.deleteAllInBatch(); categoryRepository.deleteAllInBatch(); } @@ -102,10 +102,10 @@ void returnCorrectDataWhenReadAll() { location.getLocationName()); int pageSize = 10; List favorites = new ArrayList<>(); - List chats = new ArrayList<>(); + List chatRooms = new ArrayList<>(); for (int i = 0; i < 2; i++) { favorites.add(createAndSaveFavorite(item.getId(), user.getId())); - chats.add(createAndSaveChat(user.getId(), item.getId())); + chatRooms.add(createAndSaveChat(user.getId(), item.getId())); } // when @@ -116,7 +116,7 @@ void returnCorrectDataWhenReadAll() { assertThat(itemListSlice.getCategoryName()).isEqualTo(category.getName()); assertThat(itemListSlice.getUserLocation()).isEqualTo(location.getLocationName()); assertThat(itemListSlice.getItems().get(0).getCountData().getFavorite()).isEqualTo(favorites.size()); - assertThat(itemListSlice.getItems().get(0).getCountData().getChat()).isEqualTo(chats.size()); + assertThat(itemListSlice.getItems().get(0).getCountData().getChat()).isEqualTo(chatRooms.size()); assertThat(itemListSlice.getNextCursor()).isEqualTo(null); } @@ -252,17 +252,18 @@ Item createAndSaveItem(Long categoryId, Long locationId, ItemStatus status, Long .build()); } - Message createAndSaveMessage(Long chatId) { + ChatMessage createAndSaveMessage(Long chatId) { String content = "This is a sample message."; - return messageRepository.save(Message.builder() - .chatId(chatId) + return chatMessageRepository.save(ChatMessage.builder() + .chatRoomId(chatId) .content(content) .build()); } - Chat createAndSaveChat(Long senderId, Long itemId) { + ChatRoom createAndSaveChat(Long senderId, Long itemId) { String lastMessage = "This is a sample message."; - return chatRepository.save(Chat.builder().itemId(itemId).senderId(senderId).lastMessageId(lastMessage).build()); + return chatRoomRepository.save( + ChatRoom.builder().itemId(itemId).senderId(senderId).lastMessageId(lastMessage).build()); } Category createAndSaveCategory() {