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

[refactor] 채팅 중복 체크 로직 추가 및 채팅방 멤버 리스트 관리 로직 추가 #287

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ public enum CoffeeChatErrorCode implements ErrorCode {
AUTHORITY_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.AUTHORITY_NOT_VALID, "유효하지 않는 권한"),
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_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"),;

private final HttpStatus httpStatus;
private final ServiceStatus serviceStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),

//success
COFFEE_CHAT_ROOM_CREATED(3240),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -21,4 +27,9 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, CopyOnWriteArraySet<ChatRoomMember>> 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<ChatRoomMember> getChatRoom(String roomKey) {
return chatRoomMap.getOrDefault(roomKey, new CopyOnWriteArraySet<>());
}

public ChatRoomMember removeChatRoomMember(String roomKey, Long memberId) {
Set<ChatRoomMember> chatRoomMemberList = getChatRoom(roomKey);

ChatRoomMember chatRoomMember = ChatRoomMember.builder()
.memberId(memberId)
.build();

chatRoomMemberList.remove(chatRoomMember);

return chatRoomMember;
}

public Integer countChatRoomMember(String roomKey) {
Set<ChatRoomMember> chatRoomMemberList = getChatRoom(roomKey);

return chatRoomMemberList.size();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,6 @@
public class CoffeeChatController {
private final CoffeeChatService coffeeChatService;

@PostMapping("/coffeechat/rooms")
public ResponseEntity<ApiResponse<CreateCoffeeChatRoomResponse>> createCoffeeChatRoom(
@Valid
@RequestBody
CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest
) {
CreateCoffeeChatRoomResponse response = coffeeChatService.createCoffeeChatRoom(createCoffeeChatRoomRequest);

return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_CREATED, response);
}

@PostMapping("/coffeechat/rooms/enter")
public ResponseEntity<ApiResponse<EnterCoffeeChatRoomResponse>> enterCoffeeChatRoom(
@AuthenticationPrincipal
Expand All @@ -43,17 +32,6 @@ public ResponseEntity<ApiResponse<EnterCoffeeChatRoomResponse>> enterCoffeeChatR
return ResponseEntityFactory.toResponseEntity(ROOM_ENTRY_SUCCESSFUL, response);
}

@PostMapping("/coffeechat/rooms/{roomKey}")
public ResponseEntity<ApiResponse> leaveCoffeeChatRoom(
@Valid
@PathVariable
String roomKey
) {
coffeeChatService.leaveCoffeeChatRoom(roomKey);

return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_LEAVE);
}

@GetMapping("/coffeechat/rooms/{roomKey}")
public ResponseEntity<ApiResponse<FindChatHistoryResponse>> findChatHistory(
@Valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,14 +13,22 @@
@RequiredArgsConstructor
public class MessageController {
private final KafkaTemplate<String, Object> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;

@Getter
@Builder
Expand All @@ -29,9 +30,13 @@ public class ChatMessageRequest {

private LocalDateTime sendTime;

private List<ChatRoomMember> memberList;
private Set<ChatRoomMember> memberList;

public void setMessage(String message) {
this.message = message;
}

public void setMemberList(Set<ChatRoomMember> member) {
this.memberList = member;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;

@Getter
@Builder
Expand All @@ -33,7 +34,7 @@ public class ChatMessageResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = TimeResponseFormat.PATTERN)
private LocalDateTime sendTime;

private List<ChatRoomMember> memberList;
private Set<ChatRoomMember> memberList;

public static ChatMessageResponse convertResponse(ChatMessageRequest chatMessageRequest) {
return ChatMessageResponse.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
}
}

This file was deleted.

This file was deleted.

Loading
Loading