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] 채팅방 관련 기능 구현 #143

Merged
merged 8 commits into from
Oct 4, 2023
2 changes: 2 additions & 0 deletions be/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dependencies {
testImplementation "org.testcontainers:mysql:1.19.0"
testImplementation 'io.rest-assured:rest-assured'

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

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.codesquad.secondhand.api.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // APIC으로 STOMP 테스트 시 해당 옵션 주석 처리 필요
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub");
registry.enableSimpleBroker("/sub");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kr.codesquad.secondhand.api.chat.controller;

import static kr.codesquad.secondhand.global.util.HttpAuthorizationUtils.extractMemberId;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import kr.codesquad.secondhand.api.chat.dto.ChatRoomCreateDto;
import kr.codesquad.secondhand.api.chat.dto.reponse.ChatRoomExistenceCheckResponse;
import kr.codesquad.secondhand.api.chat.dto.reponse.ChatRoomMessagesReadResponse;
import kr.codesquad.secondhand.api.chat.dto.reponse.ChatRoomReadResponse;
import kr.codesquad.secondhand.api.chat.service.ChatFacadeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class ChatRoomController {

private final ChatFacadeService chatService;

@PostMapping("/api/chat/room")
public ResponseEntity<ChatRoomCreateDto.Response> createChatRoom(HttpServletRequest httpServletRequest,
@Validated @RequestBody ChatRoomCreateDto.Request request) {
Long memberId = extractMemberId(httpServletRequest);
ChatRoomCreateDto.Response response = chatService.createChatRoom(memberId, request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(response);
}

@GetMapping("/api/chat-room/{productId}")
public ResponseEntity<ChatRoomExistenceCheckResponse> checkChatRoomExistence(HttpServletRequest httpServletRequest,
@PathVariable Long productId) {
Long memberId = extractMemberId(httpServletRequest);
ChatRoomExistenceCheckResponse response = chatService.checkChatRoomExistence(memberId, productId);
return ResponseEntity.ok(response);
}

@GetMapping("/api/chat-room")
public ResponseEntity<List<ChatRoomReadResponse>> readChatRooms(HttpServletRequest httpServletRequest) {
Long memberId = extractMemberId(httpServletRequest);
List<ChatRoomReadResponse> response = chatService.findAllChatRoomsBy(memberId);
return ResponseEntity.ok(response);
}

@GetMapping("/api/chat-room/messages")
public ResponseEntity<List<ChatRoomMessagesReadResponse>> readChatRoomMessages(@RequestParam String roomId) {
List<ChatRoomMessagesReadResponse> response = chatService.findChatRoomMessagesBy(roomId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kr.codesquad.secondhand.api.chat.domain;

import java.time.Instant;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import kr.codesquad.secondhand.api.member.domain.Member;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatMessage {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String roomId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id")
private Member sender;

private String message;

@CreationTimestamp
private Instant sentTime;

public ChatMessage(String roomId, Member sender, String message) {
this.roomId = roomId;
this.sender = sender;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package kr.codesquad.secondhand.api.chat.domain;

import java.time.Instant;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import kr.codesquad.secondhand.api.member.domain.Member;
import kr.codesquad.secondhand.api.product.domain.Product;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String roomId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "buyer_id")
private Member buyer;

@OneToOne
@JoinColumn(name = "last_message_id")
private ChatMessage lastMessage;

@CreationTimestamp
private Instant createdTime;

private ChatRoom(String roomId, Product product, Member buyer) {
this.roomId = roomId;
this.product = product;
this.buyer = buyer;
}

public static ChatRoom create(Product product, Member member) {
String id = UUID.randomUUID().toString();
return new ChatRoom(id, product, member);
}

public void updateLastMessage(ChatMessage chatMessage) {
this.lastMessage = chatMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.codesquad.secondhand.api.chat.dto;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Getter;

public class ChatRoomCreateDto {

@Getter
public static class Request {

@NotNull(message = "생성할 채팅방의 상품 정보가 비어있습니다.")
private Long productId;

@NotBlank(message = "생성할 채팅방의 메시지 내용이 없습니다.")
private String message;
}

@Getter
public static class Response {

private final String roomId;

public Response(String roomId) {
this.roomId = roomId;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package kr.codesquad.secondhand.api.chat.dto.reponse;

import lombok.Getter;

@Getter
public class ChatRoomExistenceCheckResponse {

private final String roomId;

public ChatRoomExistenceCheckResponse(String roomId) {
this.roomId = roomId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.codesquad.secondhand.api.chat.dto.reponse;

import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import kr.codesquad.secondhand.api.chat.domain.ChatMessage;
import lombok.Getter;

@Getter
public class ChatRoomMessagesReadResponse {

private final Long senderId;
private final String content;
private final Instant sentTime;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instant를 처음 보는데 사용하는 이유가 궁금해요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번에 구두로 설명드렸었는데, 간단히 설명드리자면
LocalDate, LocalTime, LocalDateTime은 타임존에 얽매이지 않는 문제가 있고 Date 클래스는 다양한 문제점이 있어 deprecated되었습니다. (왜 deprecated 되었는지는 따로 찾아보면 좋을 것 같아요!)

그래서 자바 1.8부터 제공한 Instant를 사용하게 되었습니다.
이전 코드도 수정하고 싶었는데, 시간이 없어서 기존 코드는 수정하지 못하고 새로 추가된 코드만 우선 Instant로 했습니다!


private ChatRoomMessagesReadResponse(Long senderId, String content, Instant sentTime) {
this.senderId = senderId;
this.content = content;
this.sentTime = sentTime;
}

private static ChatRoomMessagesReadResponse from(ChatMessage chatMessage) {
return new ChatRoomMessagesReadResponse(
chatMessage.getSender().getId(),
chatMessage.getMessage(),
chatMessage.getSentTime()
);
}

public static List<ChatRoomMessagesReadResponse> from(List<ChatMessage> chatMessages) {
return chatMessages.stream()
.map(ChatRoomMessagesReadResponse::from)
.collect(Collectors.toUnmodifiableList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package kr.codesquad.secondhand.api.chat.dto.reponse;

import java.net.URL;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import kr.codesquad.secondhand.api.chat.domain.ChatRoom;
import kr.codesquad.secondhand.api.member.domain.Member;
import kr.codesquad.secondhand.api.member.dto.response.MemberProfileResponse;
import kr.codesquad.secondhand.api.product.domain.Product;
import lombok.Builder;
import lombok.Getter;

@Getter
public class ChatRoomReadResponse {

private final ChatRoomDto chatRoom;
private final ProductDto product;

private ChatRoomReadResponse(ChatRoom chatRoom, Member otherMember) {
this.chatRoom = ChatRoomDto.from(chatRoom, otherMember);
this.product = ProductDto.from(chatRoom.getProduct());
}

public static List<ChatRoomReadResponse> from(List<ChatRoom> chatRooms, Map<String, Member> otherMembers) {
return chatRooms.stream()
.map(chatRoom -> {
Member otherMember = otherMembers.get(chatRoom.getRoomId());
return new ChatRoomReadResponse(chatRoom, otherMember);
})
.collect(Collectors.toList());
}

@Getter
private static class ChatRoomDto {

private final MemberProfileResponse otherMember;
private final MessageDto message;
// TODO 안 읽은 메시지 개수

private ChatRoomDto(MemberProfileResponse otherMember, MessageDto message) {
this.otherMember = otherMember;
this.message = message;
}

private static ChatRoomDto from(ChatRoom chatRoom, Member otherMember) {
return new ChatRoomDto(
MemberProfileResponse.from(otherMember),
MessageDto.from(chatRoom)
);
}
}

@Getter
private static class MessageDto {

private final String lastMessage;
private final Instant lastSentTime;

private MessageDto(String lastMessage, Instant lastSentTime) {
this.lastMessage = lastMessage;
this.lastSentTime = lastSentTime;
}

private static MessageDto from(ChatRoom chatRoom) {
return new MessageDto(
chatRoom.getLastMessage().getMessage(),
chatRoom.getLastMessage().getSentTime()
);
}
}

@Getter
private static class ProductDto {

private final Long productId;
private final String title;
private final URL thumbnailUrl;
private final Long price;
private final Integer status;

@Builder
private ProductDto(Long productId, String title, URL thumbnailUrl, Long price, Integer status) {
this.productId = productId;
this.title = title;
this.thumbnailUrl = thumbnailUrl;
this.price = price;
this.status = status;
}

private static ProductDto from(Product product) {
return ProductDto.builder()
.productId(product.getId())
.title(product.getTitle())
.thumbnailUrl(product.getThumbnailImgUrl())
.price(product.getPrice())
.status(product.getStatusId())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.codesquad.secondhand.api.chat.repository;

import java.util.List;
import kr.codesquad.secondhand.api.chat.domain.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ChatMessageRepositoryImpl extends JpaRepository<ChatMessage, String> {

// TODO 슬라이싱 하면 sentTime 기준 DESC로 정렬 필요
List<ChatMessage> findAllByRoomId(String roomId);

}
Loading