diff --git a/core/src/main/java/com/kernelsquare/core/common_response/error/code/CoffeeChatErrorCode.java b/core/src/main/java/com/kernelsquare/core/common_response/error/code/CoffeeChatErrorCode.java index 5280f527..b49bf229 100644 --- a/core/src/main/java/com/kernelsquare/core/common_response/error/code/CoffeeChatErrorCode.java +++ b/core/src/main/java/com/kernelsquare/core/common_response/error/code/CoffeeChatErrorCode.java @@ -17,6 +17,11 @@ public enum CoffeeChatErrorCode implements ErrorCode { MESSAGE_DELIVERY_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, CoffeeChatServiceStatus.MESSAGE_DELIVERY_FAILED, "토픽으로 메시지 전달 실패"), COFFEE_CHAT_ROOM_EXPIRED(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_ROOM_EXPIRED,"만료된 채팅방"), + COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED, "채팅방 정원 초과"), + CHAT_MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.CHAT_MEMBER_NOT_FOUND, "채팅방 멤버 목록에 존재하지 않는 멤버"), + MESSAGE_COMMAND_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_COMMAND_NOT_VALID, "유효하지 않는 메시지 Command"), + MESSAGE_DESTINATION_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_DESTINATION_NOT_VALID, "유효하지 않는 메시지 Destination"), + MESSAGE_NATIVEHEADER_NOT_FOUND(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_NATIVEHEADER_NOT_FOUND, "유효하지 않는 NativeHeader"), COFFEE_CHAT_SELF_REQUEST_IMPOSSIBLE(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_SELF_REQUEST_IMPOSSIBLE, "본인에게 커피챗 요청할 수 없습니다."), COFFEE_CHAT_REQUEST_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_REQUEST_NOT_VALID, diff --git a/core/src/main/java/com/kernelsquare/core/common_response/service/code/CoffeeChatServiceStatus.java b/core/src/main/java/com/kernelsquare/core/common_response/service/code/CoffeeChatServiceStatus.java index f26e0e99..18625594 100644 --- a/core/src/main/java/com/kernelsquare/core/common_response/service/code/CoffeeChatServiceStatus.java +++ b/core/src/main/java/com/kernelsquare/core/common_response/service/code/CoffeeChatServiceStatus.java @@ -11,6 +11,11 @@ public enum CoffeeChatServiceStatus implements ServiceStatus { AUTHORITY_NOT_VALID(3205), MESSAGE_DELIVERY_FAILED(3206), COFFEE_CHAT_ROOM_EXPIRED(3207), + COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED(3208), + CHAT_MEMBER_NOT_FOUND(3209), + MESSAGE_COMMAND_NOT_VALID(3210), + MESSAGE_DESTINATION_NOT_VALID(3211), + MESSAGE_NATIVEHEADER_NOT_FOUND(3212), COFFEE_CHAT_SELF_REQUEST_IMPOSSIBLE(3213), COFFEE_CHAT_REQUEST_NOT_VALID(3214), diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/common/config/SecurityConfig.java b/member-api/src/main/java/com/kernelsquare/memberapi/common/config/SecurityConfig.java index f902b666..fa19e5ac 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/common/config/SecurityConfig.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/common/config/SecurityConfig.java @@ -122,7 +122,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // ROLE_MENTOR 권한 필요 .requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/posts").hasRole("MENTOR") - .requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/rooms").hasRole("MENTOR") .requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/rooms/enter").hasAnyRole("MENTOR", "USER") .requestMatchers(HttpMethod.PUT, "/api/v1/coffeechat/posts/{postId}").hasRole("MENTOR") .requestMatchers(HttpMethod.DELETE, "/api/v1/coffeechat/posts/{postId}").hasRole("MENTOR") diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/common/config/WebSocketConfig.java b/member-api/src/main/java/com/kernelsquare/memberapi/common/config/WebSocketConfig.java index cc56aa2c..086b2f9f 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/common/config/WebSocketConfig.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/common/config/WebSocketConfig.java @@ -1,14 +1,20 @@ package com.kernelsquare.memberapi.common.config; +import com.kernelsquare.memberapi.domain.coffeechat.component.StompHandler; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; 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 +@RequiredArgsConstructor @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private final StompHandler stompHandler; + @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/kernel-square").setAllowedOriginPatterns("*").withSockJS(); @@ -21,4 +27,9 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } } diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/ChatRoomMemberManager.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/ChatRoomMemberManager.java new file mode 100644 index 00000000..93dfe7fe --- /dev/null +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/ChatRoomMemberManager.java @@ -0,0 +1,53 @@ +package com.kernelsquare.memberapi.domain.coffeechat.component; + +import com.kernelsquare.core.common_response.error.code.MemberErrorCode; +import com.kernelsquare.core.common_response.error.exception.BusinessException; +import com.kernelsquare.domainmysql.domain.member.entity.Member; +import com.kernelsquare.domainmysql.domain.member.repository.MemberRepository; +import com.kernelsquare.memberapi.domain.coffeechat.dto.ChatRoomMember; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +@Component +@RequiredArgsConstructor +public class ChatRoomMemberManager { + private final MemberRepository memberRepository; + private final ConcurrentHashMap> chatRoomMap = new ConcurrentHashMap<>(); + + public void addChatRoom(String roomKey) { + chatRoomMap.computeIfAbsent(roomKey, k -> new CopyOnWriteArraySet<>()); + } + + public void addChatMember(String roomKey, Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND)); + + chatRoomMap.computeIfAbsent(roomKey, k -> new CopyOnWriteArraySet<>()).add(ChatRoomMember.from(member)); + } + + public Set getChatRoom(String roomKey) { + return chatRoomMap.getOrDefault(roomKey, new CopyOnWriteArraySet<>()); + } + + public ChatRoomMember removeChatRoomMember(String roomKey, Long memberId) { + Set chatRoomMemberList = getChatRoom(roomKey); + + ChatRoomMember chatRoomMember = ChatRoomMember.builder() + .memberId(memberId) + .build(); + + chatRoomMemberList.remove(chatRoomMember); + + return chatRoomMember; + } + + public Integer countChatRoomMember(String roomKey) { + Set chatRoomMemberList = getChatRoom(roomKey); + + return chatRoomMemberList.size(); + } +} diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompHandler.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompHandler.java new file mode 100644 index 00000000..e4a03ba8 --- /dev/null +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompHandler.java @@ -0,0 +1,45 @@ +package com.kernelsquare.memberapi.domain.coffeechat.component; + +import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode; +import com.kernelsquare.core.common_response.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/* 메시지가 보내진 후 해당 메시지를 인터셉트하여 핸들링하기 위한 클래스 */ +@Slf4j +@Component +@RequiredArgsConstructor +public class StompHandler implements ChannelInterceptor { + private final ChatRoomMemberManager chatRoomMemberManager; + @Override + public void postSend(Message message, MessageChannel channel, boolean sent) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + StompCommand command = accessor.getCommand(); + + if (Objects.isNull(command)) { + throw new BusinessException(CoffeeChatErrorCode.MESSAGE_COMMAND_NOT_VALID); + } + + switch (command) { + case SUBSCRIBE -> handleSubscribeEvent(message); + default -> {} + } + } + + private void handleSubscribeEvent(Message message) { + StompMessageParser parser = new StompMessageParser(message); + + String roomKey = parser.getRoomKey(); + Long memberId = parser.getMemberId(); + + chatRoomMemberManager.addChatMember(roomKey, memberId); + } +} \ No newline at end of file diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompMessageParser.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompMessageParser.java new file mode 100644 index 00000000..0ebd7abe --- /dev/null +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/component/StompMessageParser.java @@ -0,0 +1,34 @@ +package com.kernelsquare.memberapi.domain.coffeechat.component; + +import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode; +import com.kernelsquare.core.common_response.error.exception.BusinessException; +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; + +import java.util.Arrays; +import java.util.Optional; + +/* STOMP 메시지 헤더에서 필요한 데이터를 가져오기 위한 클래스 */ +public class StompMessageParser { + private final StompHeaderAccessor accessor; + + public StompMessageParser(Message message) { + this.accessor = StompHeaderAccessor.wrap(message); + } + + public String getRoomKey() { + String destination = Optional.ofNullable(accessor.getDestination()) + .orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.MESSAGE_DESTINATION_NOT_VALID)); + + return Arrays.stream(destination.split("/")) + .reduce((first, second) -> second) + .get(); + } + + public Long getMemberId() { + Long memberId = Optional.ofNullable(Long.valueOf(accessor.getNativeHeader("memberId").getFirst())) + .orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.MESSAGE_NATIVEHEADER_NOT_FOUND)); + + return memberId; + } +} diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatController.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatController.java index 04802111..0ede3274 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatController.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatController.java @@ -21,17 +21,6 @@ public class CoffeeChatController { private final CoffeeChatService coffeeChatService; private final CoffeeChatFacade coffeeChatFacade; - @PostMapping("/coffeechat/rooms") - public ResponseEntity> createCoffeeChatRoom( - @Valid - @RequestBody - CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest - ) { - CreateCoffeeChatRoomResponse response = coffeeChatService.createCoffeeChatRoom(createCoffeeChatRoomRequest); - - return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_CREATED, response); - } - @PostMapping("/coffeechat/rooms/enter") public ResponseEntity> enterCoffeeChatRoom( @AuthenticationPrincipal @@ -45,17 +34,6 @@ public ResponseEntity> enterCoffeeChatR return ResponseEntityFactory.toResponseEntity(ROOM_ENTRY_SUCCESSFUL, response); } - @PostMapping("/coffeechat/rooms/{roomKey}") - public ResponseEntity leaveCoffeeChatRoom( - @Valid - @PathVariable - String roomKey - ) { - coffeeChatService.leaveCoffeeChatRoom(roomKey); - - return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_LEAVE); - } - @GetMapping("/coffeechat/rooms/{roomKey}") public ResponseEntity> findChatHistory( @Valid diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/MessageController.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/MessageController.java index 8b0bd0bd..3ae3a48e 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/MessageController.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/controller/MessageController.java @@ -2,6 +2,7 @@ import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode; import com.kernelsquare.core.common_response.error.exception.BusinessException; +import com.kernelsquare.memberapi.domain.coffeechat.component.ChatRoomMemberManager; import com.kernelsquare.memberapi.domain.coffeechat.dto.ChatMessageRequest; import lombok.RequiredArgsConstructor; import org.springframework.kafka.core.KafkaTemplate; @@ -12,14 +13,22 @@ @RequiredArgsConstructor public class MessageController { private final KafkaTemplate kafkaTemplate; + private final ChatRoomMemberManager chatRoomMemberManager; @MessageMapping("/chat/message") public void messageHandler(ChatMessageRequest message) { switch (message.getType()) { - case ENTER -> message.setMessage(message.getSender() + "님이 입장하였습니다."); + case ENTER -> { + message.setMessage(message.getSender() + "님이 입장하였습니다."); + message.setMemberList(chatRoomMemberManager.getChatRoom(message.getRoomKey())); + } case TALK -> {} case CODE -> {} - case LEAVE -> message.setMessage(message.getSender() + "님이 퇴장하였습니다."); + case LEAVE -> { + message.setMessage(message.getSender() + "님이 퇴장하였습니다."); + chatRoomMemberManager.removeChatRoomMember(message.getRoomKey(), message.getSenderId()); + message.setMemberList(chatRoomMemberManager.getChatRoom(message.getRoomKey())); + } case EXPIRE -> {} default -> throw new BusinessException(CoffeeChatErrorCode.MESSAGE_TYPE_NOT_VALID); } diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageRequest.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageRequest.java index 1e697aa7..80aa19ce 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageRequest.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageRequest.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Getter @Builder @@ -29,9 +30,13 @@ public class ChatMessageRequest { private LocalDateTime sendTime; - private List memberList; + private Set memberList; public void setMessage(String message) { this.message = message; } + + public void setMemberList(Set member) { + this.memberList = member; + } } diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageResponse.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageResponse.java index ad06bdb5..d5c95ad6 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageResponse.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatMessageResponse.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Getter @Builder @@ -33,7 +34,7 @@ public class ChatMessageResponse { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = TimeResponseFormat.PATTERN) private LocalDateTime sendTime; - private List memberList; + private Set memberList; public static ChatMessageResponse convertResponse(ChatMessageRequest chatMessageRequest) { return ChatMessageResponse.builder() diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatRoomMember.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatRoomMember.java index 58a644d4..478acb0e 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatRoomMember.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/ChatRoomMember.java @@ -1,10 +1,12 @@ package com.kernelsquare.memberapi.domain.coffeechat.dto; -import com.kernelsquare.domainmysql.domain.member.entity.Member; import com.kernelsquare.core.util.ImageUtils; +import com.kernelsquare.domainmysql.domain.member.entity.Member; import lombok.Builder; +import java.util.Objects; + @Builder public record ChatRoomMember( Long memberId, @@ -18,4 +20,18 @@ public static ChatRoomMember from(Member member) { .memberImageUrl(ImageUtils.makeImageUrl(member.getImageUrl())) .build(); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + ChatRoomMember that = (ChatRoomMember) obj; + return memberId.equals(that.memberId); + } + + @Override + public int hashCode() { + return Objects.hash(memberId); + } } \ No newline at end of file diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomRequest.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomRequest.java deleted file mode 100644 index 93c8105a..00000000 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.kernelsquare.memberapi.domain.coffeechat.dto; - -import java.util.UUID; - -import com.kernelsquare.domainmysql.domain.coffeechat.entity.ChatRoom; - -import jakarta.validation.constraints.NotBlank; -import lombok.Builder; - -@Builder -public record CreateCoffeeChatRoomRequest( - @NotBlank(message = "방 이름을 입력해 주세요.") - String roomName -) { - public static ChatRoom toEntity(CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest) { - return ChatRoom.builder() - .roomKey(UUID.randomUUID().toString()) - .build(); - } -} diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomResponse.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomResponse.java deleted file mode 100644 index 5c7d44af..00000000 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CreateCoffeeChatRoomResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kernelsquare.memberapi.domain.coffeechat.dto; - -import com.kernelsquare.domainmysql.domain.coffeechat.entity.ChatRoom; - -public record CreateCoffeeChatRoomResponse( - String roomKey -) { - public static CreateCoffeeChatRoomResponse from(ChatRoom chatRoom) { - return new CreateCoffeeChatRoomResponse( - chatRoom.getRoomKey() - ); - } -} diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/EnterCoffeeChatRoomResponse.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/EnterCoffeeChatRoomResponse.java index edc284a0..b4f19389 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/EnterCoffeeChatRoomResponse.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/EnterCoffeeChatRoomResponse.java @@ -7,6 +7,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Builder public record EnterCoffeeChatRoomResponse( @@ -16,21 +17,17 @@ public record EnterCoffeeChatRoomResponse( Boolean active, - List memberList, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = TimeResponseFormat.PATTERN) LocalDateTime expirationTime ) { public static EnterCoffeeChatRoomResponse of( String articleTitle, - ChatRoom chatRoom, - List chatRoomMemberList + ChatRoom chatRoom ) { return EnterCoffeeChatRoomResponse.builder() .articleTitle(articleTitle) .roomKey(chatRoom.getRoomKey()) .active(chatRoom.getActive()) - .memberList(chatRoomMemberList) .expirationTime(chatRoom.getExpirationTime()) .build(); } diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/FindMongoChatMessage.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/FindMongoChatMessage.java index e4b68dd0..103f8afd 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/FindMongoChatMessage.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/dto/FindMongoChatMessage.java @@ -10,22 +10,22 @@ @Builder public record FindMongoChatMessage( - MongoMessageType type, - String roomKey, - + MongoMessageType type, + Long senderId, String sender, - + String senderImageUrl, String message, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = TimeResponseFormat.PATTERN) LocalDateTime sendTime ) { public static FindMongoChatMessage from(MongoChatMessage message) { return FindMongoChatMessage.builder() - .type(message.getType()) .roomKey(message.getRoomKey()) + .type(message.getType()) + .senderId(message.getSenderId()) .sender(message.getSender()) + .senderImageUrl(message.getSenderImageUrl()) .message(message.getMessage()) .sendTime(message.getSendTime()) .build(); diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatService.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatService.java index 6f1853fd..e698c306 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatService.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatService.java @@ -3,50 +3,39 @@ import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode; import com.kernelsquare.core.common_response.error.code.ReservationErrorCode; import com.kernelsquare.core.common_response.error.exception.BusinessException; +import com.kernelsquare.core.type.MessageType; import com.kernelsquare.domainmongodb.domain.coffeechat.repository.MongoChatMessageRepository; import com.kernelsquare.domainmysql.domain.coffeechat.command.CoffeeChatCommand; import com.kernelsquare.domainmysql.domain.coffeechat.entity.ChatRoom; import com.kernelsquare.domainmysql.domain.coffeechat.info.CoffeeChatInfo; -import com.kernelsquare.domainmysql.domain.coffeechat.repository.CoffeeChatRepository; import com.kernelsquare.domainmysql.domain.member.entity.Member; import com.kernelsquare.domainmysql.domain.member.repository.MemberReader; import com.kernelsquare.domainmysql.domain.reservation.entity.Reservation; import com.kernelsquare.domainmysql.domain.reservation.repository.ReservationRepository; import com.kernelsquare.memberapi.domain.auth.dto.MemberAdapter; +import com.kernelsquare.memberapi.domain.coffeechat.component.ChatRoomMemberManager; import com.kernelsquare.memberapi.domain.coffeechat.dto.*; import com.kernelsquare.memberapi.domain.coffeechat.validation.CoffeeChatValidation; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; +import java.time.LocalDateTime; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class CoffeeChatService { - private final CoffeeChatRepository coffeeChatRepository; private final MongoChatMessageRepository mongoChatMessageRepository; private final ReservationRepository reservationRepository; - private final MemberReader memberReader; - - private final ConcurrentHashMap> chatRoomMemberList = new ConcurrentHashMap<>(); - - @Transactional - public CreateCoffeeChatRoomResponse createCoffeeChatRoom(CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest) { - ChatRoom chatRoom = CreateCoffeeChatRoomRequest.toEntity(createCoffeeChatRoomRequest); - - ChatRoom saveChatRoom = coffeeChatRepository.save(chatRoom); - - return CreateCoffeeChatRoomResponse.from(saveChatRoom); - } + private final MemberReader memberReader; + private final ChatRoomMemberManager chatRoomMemberManager; + private final KafkaTemplate kafkaTemplate; @Transactional public EnterCoffeeChatRoomResponse enterCoffeeChatRoom(EnterCoffeeChatRoomRequest enterCoffeeChatRoomRequest, - MemberAdapter memberAdapter) { + MemberAdapter memberAdapter) { Reservation reservation = reservationRepository.findById(enterCoffeeChatRoomRequest.reservationId()) .orElseThrow(() -> new BusinessException(ReservationErrorCode.RESERVATION_NOT_FOUND)); @@ -71,33 +60,55 @@ public EnterCoffeeChatRoomResponse mentorEnter(EnterCoffeeChatRoomRequest enterC chatRoom.activateRoom(enterCoffeeChatRoomRequest.articleTitle()); - //TODO 중복 입장에 대한 정책이 정해지면 로직 구현 - chatRoomMemberList.computeIfAbsent(chatRoom.getRoomKey(), k -> new ArrayList<>()) - .add(ChatRoomMember.from(memberAdapter.getMember())); + chatRoomMemberManager.addChatRoom(chatRoom.getRoomKey()); + + CoffeeChatValidation.validateChatRoomCapacity(chatRoomMemberManager, chatRoom); + + if (Boolean.TRUE.equals(CoffeeChatValidation.validateDuplicateEntry(chatRoomMemberManager, chatRoom, memberAdapter))) { + ChatRoomMember chatRoomMember = chatRoomMemberManager.removeChatRoomMember(chatRoom.getRoomKey(), memberAdapter.getMember().getId()); - return EnterCoffeeChatRoomResponse.of(enterCoffeeChatRoomRequest.articleTitle(), chatRoom, - chatRoomMemberList.get(chatRoom.getRoomKey())); + ChatMessageRequest message = ChatMessageRequest.builder() + .type(MessageType.LEAVE) + .roomKey(chatRoom.getRoomKey()) + .senderId(chatRoomMember.memberId()) + .sender(chatRoomMember.nickname()) + .senderImageUrl(chatRoomMember.memberImageUrl()) + .message(chatRoomMember.nickname() + "님이 퇴장하였습니다.") + .sendTime(LocalDateTime.now()) + .memberList(chatRoomMemberManager.getChatRoom(chatRoom.getRoomKey())) + .build(); + + kafkaTemplate.send("chat", message); + } + + return EnterCoffeeChatRoomResponse.of(enterCoffeeChatRoomRequest.articleTitle(), chatRoom); } public EnterCoffeeChatRoomResponse menteeEnter(EnterCoffeeChatRoomRequest enterCoffeeChatRoomRequest, - ChatRoom chatRoom, MemberAdapter memberAdapter) { + ChatRoom chatRoom, MemberAdapter memberAdapter) { + CoffeeChatValidation.validateChatRoomActive(chatRoom); - //TODO 중복 입장에 대한 정책이 정해지면 로직 구현 - chatRoomMemberList.get(chatRoom.getRoomKey()).add(ChatRoomMember.from(memberAdapter.getMember())); + CoffeeChatValidation.validateChatRoomCapacity(chatRoomMemberManager, chatRoom); - return EnterCoffeeChatRoomResponse.of(enterCoffeeChatRoomRequest.articleTitle(), chatRoom, - chatRoomMemberList.get(chatRoom.getRoomKey())); - } + if (Boolean.TRUE.equals(CoffeeChatValidation.validateDuplicateEntry(chatRoomMemberManager, chatRoom, memberAdapter))) { + ChatRoomMember chatRoomMember = chatRoomMemberManager.removeChatRoomMember(chatRoom.getRoomKey(), memberAdapter.getMember().getId()); - @Transactional - public void leaveCoffeeChatRoom(String roomKey) { - ChatRoom chatRoom = coffeeChatRepository.findByRoomKey(roomKey) - .orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.COFFEE_CHAT_ROOM_NOT_FOUND)); + ChatMessageRequest message = ChatMessageRequest.builder() + .type(MessageType.LEAVE) + .roomKey(chatRoom.getRoomKey()) + .senderId(chatRoomMember.memberId()) + .sender(chatRoomMember.nickname()) + .senderImageUrl(chatRoomMember.memberImageUrl()) + .message(chatRoomMember.nickname() + "님이 퇴장하였습니다.") + .sendTime(LocalDateTime.now()) + .memberList(chatRoomMemberManager.getChatRoom(chatRoom.getRoomKey())) + .build(); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + kafkaTemplate.send("chat", message); + } - //TODO 특정 채팅방의 유저 리스트가 필요하다면? + return EnterCoffeeChatRoomResponse.of(enterCoffeeChatRoomRequest.articleTitle(), chatRoom); } public FindChatHistoryResponse findChatHistory(String roomKey) { diff --git a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/validation/CoffeeChatValidation.java b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/validation/CoffeeChatValidation.java index 62a0bbc5..f988f36c 100644 --- a/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/validation/CoffeeChatValidation.java +++ b/member-api/src/main/java/com/kernelsquare/memberapi/domain/coffeechat/validation/CoffeeChatValidation.java @@ -10,9 +10,12 @@ import com.kernelsquare.domainmysql.domain.member_authority.entity.MemberAuthority; import com.kernelsquare.domainmysql.domain.reservation.entity.Reservation; import com.kernelsquare.memberapi.domain.auth.dto.MemberAdapter; +import com.kernelsquare.memberapi.domain.coffeechat.component.ChatRoomMemberManager; +import com.kernelsquare.memberapi.domain.coffeechat.dto.ChatRoomMember; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; public class CoffeeChatValidation { @@ -50,6 +53,31 @@ public static ReservationMemberType validatePermission(Reservation reservation, return ReservationMemberType.OTHER; } + public static Boolean validateDuplicateEntry(ChatRoomMemberManager manager, + ChatRoom chatRoom, MemberAdapter memberAdapter) { + String roomKey = chatRoom.getRoomKey(); + + Set chatMemberSet = manager.getChatRoom(roomKey); + + Member member = memberAdapter.getMember(); + + ChatRoomMember chatRoomMember = ChatRoomMember.from(member); + + if (chatMemberSet.contains(chatRoomMember)) { + return Boolean.TRUE; + } + + return Boolean.FALSE; + } + + public static void validateChatRoomCapacity(ChatRoomMemberManager manager, ChatRoom chatRoom) { + Integer memberCount = manager.countChatRoomMember(chatRoom.getRoomKey()); + + if (memberCount >= 2) { + throw new BusinessException(CoffeeChatErrorCode.COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED); + } + } + public static void validateCoffeeChatRequest(Member sender, Member recipient) { if (sender.getId().equals(recipient.getId())) { throw new BusinessException(CoffeeChatErrorCode.COFFEE_CHAT_SELF_REQUEST_IMPOSSIBLE); diff --git a/member-api/src/main/resources/templates/chatscreen.html b/member-api/src/main/resources/templates/chatscreen.html index 4c1f6f2a..b4e74f93 100644 --- a/member-api/src/main/resources/templates/chatscreen.html +++ b/member-api/src/main/resources/templates/chatscreen.html @@ -87,7 +87,7 @@

('0' + now.getSeconds()).slice(-2) + '.' + ('00' + now.getMilliseconds()).slice(-3) + 'Z'; ws.send("/app/chat/message", {}, JSON.stringify({type:'LEAVE', room_key:this.roomId, sender:this.sender, send_time:sendTime})); - ws.disconnect(); + ws.disconnect({}, {memberId:"1", roomKey:roomId}); } } }); @@ -97,7 +97,7 @@

ws.subscribe("/topic/chat/room/"+vm.$data.roomId, function(message) { var recv = JSON.parse(message.body); vm.recvMessage(recv); - }); + }, {memberId:"1"}); var now = new Date(); var sendTime = now.getFullYear() + '-' + ('0' + (now.getMonth()+1)).slice(-2) + '-' + diff --git a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatControllerTest.java b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatControllerTest.java index 6b989d6e..f66c2c43 100644 --- a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatControllerTest.java +++ b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/controller/CoffeeChatControllerTest.java @@ -59,44 +59,6 @@ class CoffeeChatControllerTest { private ObjectMapper objectMapper = new ObjectMapper(); - @Test - @DisplayName("채팅방 생성 성공시 200 OK와 메시지를 반환한다") - void testCreateCoffeeChatRoom() throws Exception { - //given - String roomName = "불꽃남자의 예절 주입방"; - - CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest = CreateCoffeeChatRoomRequest.builder() - .roomName(roomName) - .build(); - - ChatRoom chatRoom = CreateCoffeeChatRoomRequest.toEntity(createCoffeeChatRoomRequest); - - ChatRoom saveChatRoom = ChatRoom.builder() - .id(1L) - .roomKey(chatRoom.getRoomKey()) - .build(); - - CreateCoffeeChatRoomResponse createCoffeeChatRoomResponse = CreateCoffeeChatRoomResponse.from(saveChatRoom); - - given(coffeeChatService.createCoffeeChatRoom(any(CreateCoffeeChatRoomRequest.class))).willReturn( - createCoffeeChatRoomResponse); - - objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); - String jsonRequest = objectMapper.writeValueAsString(createCoffeeChatRoomRequest); - - //when & then - mockMvc.perform(post("/api/v1/coffeechat/rooms") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .characterEncoding("UTF-8") - .content(jsonRequest)) - .andExpect(status().is(COFFEE_CHAT_ROOM_CREATED.getStatus().value())) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.code").value(COFFEE_CHAT_ROOM_CREATED.getCode())) - .andExpect(jsonPath("$.msg").value(COFFEE_CHAT_ROOM_CREATED.getMsg())); - } - @Test @DisplayName("채팅방 입장 성공시 200 OK와 메시지를 반환한다") void testEnterCoffeeChatRoom() throws Exception { @@ -134,10 +96,8 @@ void testEnterCoffeeChatRoom() throws Exception { MemberAdapter memberAdapter = new MemberAdapter(MemberAdaptorInstance.of(member)); - ChatRoomMember chatRoomMember = ChatRoomMember.from(member); - EnterCoffeeChatRoomResponse enterCoffeeChatRoomResponse = EnterCoffeeChatRoomResponse.of( - enterCoffeeChatRoomRequest.articleTitle(), chatRoom, List.of(chatRoomMember)); + enterCoffeeChatRoomRequest.articleTitle(), chatRoom); given(coffeeChatService.enterCoffeeChatRoom(any(EnterCoffeeChatRoomRequest.class), any(MemberAdapter.class))).willReturn( enterCoffeeChatRoomResponse); diff --git a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CoffeeChatRequestDtoTest.java b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CoffeeChatRequestDtoTest.java index 6cbf7d5e..dcd52d72 100644 --- a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CoffeeChatRequestDtoTest.java +++ b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/dto/CoffeeChatRequestDtoTest.java @@ -18,21 +18,6 @@ class CoffeeChatRequestDtoTest { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); - @Test - @DisplayName("채팅방 생성 요청 검증 테스트") - void validateCreateCoffeeChatRoomRequest() { - CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest = CreateCoffeeChatRoomRequest.builder() - .roomName("") - .build(); - - Set> violations = validator.validate( - createCoffeeChatRoomRequest); - Set msgList = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toSet()); - - //then - assertThat(msgList).isEqualTo(Set.of("방 이름을 입력해 주세요.")); - } - @Test @DisplayName("채팅방 입장 요청 검증 테스트") void validateEnterCoffeeChatRoomRequest() { diff --git a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatServiceTest.java b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatServiceTest.java index f7883fd0..9d405456 100644 --- a/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatServiceTest.java +++ b/member-api/src/test/java/com/kernelsquare/memberapi/domain/coffeechat/service/CoffeeChatServiceTest.java @@ -14,6 +14,7 @@ import com.kernelsquare.domainmysql.domain.reservation_article.entity.ReservationArticle; import com.kernelsquare.memberapi.domain.auth.dto.MemberAdapter; import com.kernelsquare.memberapi.domain.auth.dto.MemberAdaptorInstance; +import com.kernelsquare.memberapi.domain.coffeechat.component.ChatRoomMemberManager; import com.kernelsquare.memberapi.domain.coffeechat.dto.EnterCoffeeChatRoomRequest; import com.kernelsquare.memberapi.domain.coffeechat.dto.EnterCoffeeChatRoomResponse; import com.kernelsquare.memberapi.domain.coffeechat.dto.FindChatHistoryResponse; @@ -45,6 +46,8 @@ class CoffeeChatServiceTest { private ReservationRepository reservationRepository; @Mock private MongoChatMessageRepository mongoChatMessageRepository; + @Mock + private ChatRoomMemberManager chatRoomMemberManager; @Test @DisplayName("채팅방 입장 테스트") @@ -125,6 +128,8 @@ void testEnterCoffeeChatRoom() { given(reservationRepository.findById(anyLong())).willReturn(Optional.of(reservation)); + doNothing().when(chatRoomMemberManager).addChatRoom(anyString()); + //when EnterCoffeeChatRoomResponse response = coffeeChatService.enterCoffeeChatRoom(enterCoffeeChatRoomRequest, memberAdapter);