From 4903c5f2981e79b07a53440c011974713fd59f63 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Tue, 4 Feb 2025 15:18:31 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feature/Kafka=20+=20WebSocket=EC=9D=84=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=86=A1=EC=88=98=EC=8B=A0=20=EA=B8=B0=EB=8A=A5=20=EC=83=98?= =?UTF-8?q?=ED=94=8C=20=EA=B5=AC=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메시지 전송을 위한 Kafka 프로듀서 구현 - Kafka 리스너에서 메시지 수신 후 WebSocket으로 전달 --- chat-service/build.gradle | 6 +++ .../chat/controller/ChatController.java | 19 ++++++++ .../chatservice/domain/chat/dto/Message.java | 16 +++++++ .../domain/chat/service/ChatService.java | 26 +++++++++++ .../domain/chat/service/MessageReceiver.java | 24 +++++++++++ .../domain/chat/service/MessageSender.java | 21 +++++++++ .../websocket/WebSocketConfiguration.java | 43 +++++++++++++++++++ .../src/main/resources/application-local.yml | 12 ++++++ chat-service/src/main/resources/test.html | 38 ++++++++++++++++ 9 files changed, 205 insertions(+) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageSender.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java create mode 100644 chat-service/src/main/resources/test.html diff --git a/chat-service/build.gradle b/chat-service/build.gradle index 2054976..0a563ea 100644 --- a/chat-service/build.gradle +++ b/chat-service/build.gradle @@ -6,9 +6,15 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-config-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // dynamodb implementation platform('software.amazon.awssdk:bom:2.20.85') implementation 'software.amazon.awssdk:dynamodb-enhanced' + // kafka & websocket + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + testImplementation 'org.springframework.kafka:spring-kafka-test' + runtimeOnly 'org.postgresql:postgresql:42.7.5' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java new file mode 100644 index 0000000..20821a0 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -0,0 +1,19 @@ +package com.rljj.chatservice.domain.chat.controller; + +import com.rljj.chatservice.domain.chat.dto.Message; +import com.rljj.chatservice.domain.chat.service.ChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ChatController { + private final ChatService chatService; + + @MessageMapping("/messages") + public void sendMessage(Message message) { + chatService.sendMessage(message); + } + +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java new file mode 100644 index 0000000..9151fc9 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java @@ -0,0 +1,16 @@ +package com.rljj.chatservice.domain.chat.dto; + +import lombok.*; +import java.io.Serializable; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Message implements Serializable { + private String id; + private Integer chatNo; + private String content; +} + + diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java new file mode 100644 index 0000000..d1be3a0 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -0,0 +1,26 @@ +package com.rljj.chatservice.domain.chat.service; + +import com.rljj.chatservice.domain.chat.dto.Message; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ChatService { + + private final MessageSender sender; + + public void sendMessage(Message message) { + // 1. accessToken으로 member 찾고 + + // 2. message 객체에 보낸시간, 보낸사람 memberNo, 닉네임을 셋팅해준다. + //message.setSendTimeAndSender(LocalDateTime.now(), findMember.getMemberNo(), findMember.getNickname(), readCount); + + // 3. 메시지를 전송한다. + sender.send("chat", message); // TOPIC: chat 임시 설정 + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java new file mode 100644 index 0000000..e28e471 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java @@ -0,0 +1,24 @@ +package com.rljj.chatservice.domain.chat.service; + +import com.rljj.chatservice.domain.chat.dto.Message; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MessageReceiver { + private final SimpMessageSendingOperations template; + + @KafkaListener(topics = "chat", autoStartup = "true") + public void receiveMessage(Message message) { + log.info("전송 위치 = /sub/public/"+ message.getChatNo()); + log.info("채팅 방으로 메시지 전송 = {}", message); + + // 메시지객체 내부의 채팅방번호를 참조하여, 해당 채팅방 구독자에게 메시지를 발송한다. + template.convertAndSend("/sub/public/" + message.getChatNo(), message); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageSender.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageSender.java new file mode 100644 index 0000000..7db7d56 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageSender.java @@ -0,0 +1,21 @@ +package com.rljj.chatservice.domain.chat.service; + +import com.rljj.chatservice.domain.chat.dto.Message; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MessageSender { + private final KafkaTemplate kafkaTemplate; + + // 메시지를 지정한 Kafka 토픽으로 전송 + public void send(String topic, Message data) { + + // KafkaTemplate을 사용하여 메시지를 지정된 토픽으로 전송 + kafkaTemplate.send(topic, data); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java new file mode 100644 index 0000000..77cee9e --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java @@ -0,0 +1,43 @@ +package com.rljj.chatservice.global.config.websocket; + +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; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + // STOMP 엔드포인트를 등록하는 메서드 + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/chat") // STOMP 엔드포인트 설정 + .setAllowedOriginPatterns("*") // 모든 Origin 허용 + .withSockJS(); // SockJS 사용가능 설정 + } + + // 메시지 브로커를 구성하는 메서드 + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/sub"); // /sub/{chatNo}로 주제 구독 요청: 메시지 송신 + registry.setApplicationDestinationPrefixes("/pub"); // /pub/message로 메시지 전송 컨트롤러 라우팅 가능: 메시지 수신 + } + +/* // 클라이언트 인바운드 채널을 구성하는 메서드 + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + // stompHandler를 인터셉터로 등록하여 STOMP 메시지 핸들링을 수행 + registration.interceptors(stompHandler); + }*/ + + // STOMP에서 64KB 이상의 데이터 전송을 못하는 문제 해결 + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setMessageSizeLimit(160 * 64 * 1024); + registry.setSendTimeLimit(100 * 10000); + registry.setSendBufferSizeLimit(3 * 512 * 1024); + } +} diff --git a/chat-service/src/main/resources/application-local.yml b/chat-service/src/main/resources/application-local.yml index d9ba179..a9acd05 100644 --- a/chat-service/src/main/resources/application-local.yml +++ b/chat-service/src/main/resources/application-local.yml @@ -2,6 +2,18 @@ spring: config: import: - configserver:http://localhost:8888 + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: chat-group + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer aws: dynamodb: diff --git a/chat-service/src/main/resources/test.html b/chat-service/src/main/resources/test.html new file mode 100644 index 0000000..aa13189 --- /dev/null +++ b/chat-service/src/main/resources/test.html @@ -0,0 +1,38 @@ + + + + + WebSocket Test + + + + + +

WebSocket 테스트

+ + + + + + From 6590c951d39fbc070b46a868d5b9d1c761c68846 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Wed, 5 Feb 2025 14:14:54 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feature/=EC=B1=84=ED=8C=85=EB=B0=A9=20CR?= =?UTF-8?q?=20=EA=B0=9C=EC=9A=94=20=EA=B5=AC=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 34 ++++++++++++++++- .../domain/chat/dto/ChatRoomRequest.java | 22 +++++++++++ .../domain/chat/dto/ChatRoomResponse.java | 16 ++++++++ .../chat/respository/ChatRoomRepository.java | 21 ++++++++++ .../domain/chat/service/ChatService.java | 38 +++++++++++++++++++ .../baseentity/BaseEntity.java | 2 + .../switchswitchentity/chat/ChatRoom.java | 21 +++++++--- .../chip/chippost/ChipPost.java | 3 +- .../switchswitchentity/member/Member.java | 3 +- 9 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 20821a0..4c89cb9 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -1,16 +1,48 @@ package com.rljj.chatservice.domain.chat.controller; +import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; +import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; import com.rljj.chatservice.domain.chat.dto.Message; import com.rljj.chatservice.domain.chat.service.ChatService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RequestMapping("/api/chat") @RestController @RequiredArgsConstructor public class ChatController { private final ChatService chatService; + // TODO 파라미터 memberId 임시 -> security로 처리할 것 + // 채팅방 만들기 + @PostMapping("/rooms") + public ResponseEntity createChatRoom( + @RequestParam Long memberId, + @RequestBody ChatRoomRequest request) { + chatService.makeChatRoom(memberId, request); + return ResponseEntity.ok().build(); + } + + // TODO 일단 all 조회로 만들기 -> 페이징 처리 어캐 할지 + // 채팅방 리스트 조회하기 + @GetMapping("/rooms") + public ResponseEntity> chatRoomList( + @RequestParam Long memberId) { + List chatRoomList = chatService.getChatRoomList(memberId); + return ResponseEntity.ok(chatRoomList); + } + + // 채팅방 삭제하기 + + // 채팅방 접속 끊기 + + // 채팅메시지 내역 조회하기 + + // 채팅메시지 보내기 @MessageMapping("/messages") public void sendMessage(Message message) { chatService.sendMessage(message); diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java new file mode 100644 index 0000000..bb7d18d --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java @@ -0,0 +1,22 @@ +package com.rljj.chatservice.domain.chat.dto; + +import com.rljj.switchswitchentity.chat.ChatRoom; +import com.rljj.switchswitchentity.chip.chippost.ChipPost; +import com.rljj.switchswitchentity.member.Member; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ChatRoomRequest { + private Long chipPostId; // 게시물 id + private Long requesterId; // 거래요청건 멤버 id + + public ChatRoom toEntity(Long memberId) { + return ChatRoom.create( + ChipPost.builder().id(chipPostId).build(), + Member.builder().id(memberId).build(), + Member.builder().id(requesterId).build() + ); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java new file mode 100644 index 0000000..9557365 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java @@ -0,0 +1,16 @@ +package com.rljj.chatservice.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ChatRoomResponse { + + private Long chatRoomId; + private Long memberId; // 현재 멤버 ID + private Long chatPartnerId; // 채팅 상대방 ID + private Long chipPostId; // 게시물 ID +// private String latestMessage; +// private LocalDateTime latestMessageCreatedAt; +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java new file mode 100644 index 0000000..aa5884a --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java @@ -0,0 +1,21 @@ +package com.rljj.chatservice.domain.chat.respository; + +import com.rljj.switchswitchentity.chat.ChatRoom; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ChatRoomRepository extends JpaRepository { + + @Query("SELECT c FROM ChatRoom c " + + "JOIN FETCH c.creatorUser " + + "JOIN FETCH c.interestedUser " + + "JOIN FETCH c.chipPost " + + "WHERE c.creatorUser.id = :memberId OR c.interestedUser.id = :memberId") + List findChatRoomsByMemberId(@Param("memberId") Long memberId); + +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index d1be3a0..f635b9a 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -1,11 +1,17 @@ package com.rljj.chatservice.domain.chat.service; +import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; +import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; import com.rljj.chatservice.domain.chat.dto.Message; +import com.rljj.chatservice.domain.chat.respository.ChatRoomRepository; +import com.rljj.switchswitchentity.chat.ChatRoom; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Slf4j @Service @Transactional(readOnly = true) @@ -13,6 +19,37 @@ public class ChatService { private final MessageSender sender; + private final ChatRoomRepository chatRoomRepository; + + // 채팅방 만들기 + @Transactional + public void makeChatRoom(Long memberId, ChatRoomRequest requestDto) { + // 1. 유효성 체크 + + // 2. 저장 + chatRoomRepository.save(requestDto.toEntity(memberId)); + } + + // 채팅방 리스트 조회 + public List getChatRoomList(Long memberId) { + List chatRoomList = chatRoomRepository.findChatRoomsByMemberId(memberId); + + List chatRoomResponseList = chatRoomList.stream() + .map(chatRoom -> new ChatRoomResponse( + chatRoom.getId(), + memberId, // 현재 로그인한 사용자 ID + chatRoom.getCreatorUser().getId().equals(memberId) + ? chatRoom.getInterestedUser().getId() // 내가 creatorUser면 상대방 = interestedUser + : chatRoom.getCreatorUser().getId(), // 내가 interestedUser면 상대방 = creatorUser + chatRoom.getChipPost().getId() + )) + .toList(); + + // TODO 다이나모DB 조회해서 마지막 메시지 resposne에 넣어줘야 함 + return chatRoomResponseList; + } + + public void sendMessage(Message message) { // 1. accessToken으로 member 찾고 @@ -23,4 +60,5 @@ public void sendMessage(Message message) { // 3. 메시지를 전송한다. sender.send("chat", message); // TOPIC: chat 임시 설정 } + } diff --git a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/baseentity/BaseEntity.java b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/baseentity/BaseEntity.java index f49a1da..246dcbf 100644 --- a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/baseentity/BaseEntity.java +++ b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/baseentity/BaseEntity.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -17,6 +18,7 @@ @NoArgsConstructor @EntityListeners(AuditingEntityListener.class) @MappedSuperclass +@SuperBuilder public abstract class BaseEntity { @Id diff --git a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java index c04b0af..e63a720 100644 --- a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java +++ b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java @@ -4,25 +4,22 @@ import com.rljj.switchswitchentity.chip.chippost.ChipPost; import com.rljj.switchswitchentity.member.Member; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; import lombok.*; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder public class ChatRoom extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @NonNull private ChipPost chipPost; + // TODO creatorUser -> author, @ManyToOne(fetch = FetchType.LAZY) @NonNull private Member creatorUser; + // TODO interestedUser -> requester로 이름 바꾸기 @ManyToOne(fetch = FetchType.LAZY) @NonNull private Member interestedUser; @@ -30,5 +27,17 @@ public class ChatRoom extends BaseEntity { @Enumerated(EnumType.STRING) @NonNull private ChatRoomStatus status; + + public static ChatRoom create(ChipPost chipPost, Member author, Member requester) { + return new ChatRoom(chipPost, author, requester); + } + + @Builder + private ChatRoom(ChipPost chipPost, Member author, Member requester) { + this.chipPost = chipPost; + this.creatorUser = author; + this.interestedUser = requester; + this.status = ChatRoomStatus.ACTIVE; + } } diff --git a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chip/chippost/ChipPost.java b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chip/chippost/ChipPost.java index 45b1f2d..cfb0752 100644 --- a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chip/chippost/ChipPost.java +++ b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chip/chippost/ChipPost.java @@ -5,12 +5,13 @@ import com.rljj.switchswitchentity.member.Member; import jakarta.persistence.*; import lombok.*; +import lombok.experimental.SuperBuilder; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Builder +@SuperBuilder public class ChipPost extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @NonNull diff --git a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/member/Member.java b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/member/Member.java index 9f76dab..55d5dfe 100644 --- a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/member/Member.java +++ b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/member/Member.java @@ -4,12 +4,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import lombok.*; +import lombok.experimental.SuperBuilder; @Getter @Setter @AllArgsConstructor @NoArgsConstructor -@Builder +@SuperBuilder @Entity public class Member extends BaseEntity { @Column(unique = true) From 334b5dcbf3b9c146d6f67120845ca39111f67180 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Wed, 5 Feb 2025 18:34:12 +0900 Subject: [PATCH 03/11] =?UTF-8?q?Feature/=EC=B1=84=ED=8C=85=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EA=B0=9C=EC=9A=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 18 ++-- .../controller/ChatMessageController.java | 29 ------ .../domain/chat/dto/ChatMessageDto.java | 36 ++++++++ .../chat/dto/ChatMessageListResponse.java | 12 +++ .../respository/ChatMessageRepository.java | 92 ++++++++++++++++++- .../domain/chat/service/ChatService.java | 20 ++-- 6 files changed, 161 insertions(+), 46 deletions(-) delete mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatMessageController.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageDto.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageListResponse.java diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 4c89cb9..8e72cac 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -1,8 +1,6 @@ package com.rljj.chatservice.domain.chat.controller; -import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; -import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; -import com.rljj.chatservice.domain.chat.dto.Message; +import com.rljj.chatservice.domain.chat.dto.*; import com.rljj.chatservice.domain.chat.service.ChatService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -30,17 +28,23 @@ public ResponseEntity createChatRoom( // TODO 일단 all 조회로 만들기 -> 페이징 처리 어캐 할지 // 채팅방 리스트 조회하기 @GetMapping("/rooms") - public ResponseEntity> chatRoomList( + public ResponseEntity> getChatRoomList( @RequestParam Long memberId) { List chatRoomList = chatService.getChatRoomList(memberId); return ResponseEntity.ok(chatRoomList); } - // 채팅방 삭제하기 - - // 채팅방 접속 끊기 + // TODO 채팅방 삭제하기 + // TODO 채팅방 접속 끊기 // 채팅메시지 내역 조회하기 + @GetMapping("/rooms/{roomId}") + public ResponseEntity getChatMessageList( + @PathVariable("roomId") Long roomId, + @RequestParam Long memberId) { + ChatMessageListResponse chatMessageList = chatService.getChatMessageList(roomId, memberId); + return ResponseEntity.ok(chatMessageList); + } // 채팅메시지 보내기 @MessageMapping("/messages") diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatMessageController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatMessageController.java deleted file mode 100644 index 63f2418..0000000 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatMessageController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.rljj.chatservice.domain.chat.controller; - -import com.rljj.chatservice.domain.chat.respository.ChatMessageRepository; -import com.rljj.chatservice.domain.chat.model.ChatMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/chat") -public class ChatMessageController { - - private final ChatMessageRepository chatMessageRepository; - - @PostMapping("/chatMessage") - public ResponseEntity saveChatMessage(@RequestBody ChatMessage chatMessage) { - chatMessageRepository.saveChatMessage(chatMessage); - return ResponseEntity.ok().build(); - } - - @GetMapping("/chatRoom/{chatRoomId}") - public List getChatRoomById(@PathVariable("chatRoomId") Long chatRoomId) { - return chatMessageRepository.getChatRoomById(chatRoomId); - } - -} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageDto.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageDto.java new file mode 100644 index 0000000..b07a0f6 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageDto.java @@ -0,0 +1,36 @@ +package com.rljj.chatservice.domain.chat.dto; + +import com.rljj.chatservice.domain.chat.model.ChatMessage; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class ChatMessageDto { + private String chatMessageId; + + private Long chatRoomId; + private LocalDateTime sendDate; + + private Long senderId; + private String message; + + private boolean isMine; + + public ChatMessageDto(ChatMessage chatMessage, Long memberId) { + this.chatMessageId = chatMessage.getMessageId(); + this.chatRoomId = chatMessage.getChatRoomId(); + this.sendDate = chatMessage.getCreatedAt(); + this.senderId = chatMessage.getSenderId(); + this.message = chatMessage.getMessage(); + this.isMine = chatMessage.getSenderId().equals(memberId); // 내가 보낸 메시지 여부 + } + + public static List from(List chatMessageList, Long memberId) { + return chatMessageList.stream() + .map(chatMessage -> new ChatMessageDto(chatMessage, memberId)) + .toList(); + } + +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageListResponse.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageListResponse.java new file mode 100644 index 0000000..5ede0f8 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatMessageListResponse.java @@ -0,0 +1,12 @@ +package com.rljj.chatservice.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class ChatMessageListResponse { + private List messages; +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatMessageRepository.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatMessageRepository.java index d2e2ea9..e0b5f43 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatMessageRepository.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatMessageRepository.java @@ -24,11 +24,11 @@ public void saveChatMessage(ChatMessage chatMessage) { chatMessageTable.putItem(chatMessage); } - public List getChatRoomById(Long chatRoomId) { + public List findByChatRoomId(Long chatRoomId) { // Partition Key 조건 생성 QueryConditional queryConditional = QueryConditional.keyEqualTo( - Key.builder().partitionValue(chatRoomId).build() - ); + Key.builder().partitionValue(chatRoomId) + .build()); // Query 실행 return chatMessageTable.query(r -> r @@ -37,4 +37,90 @@ public List getChatRoomById(Long chatRoomId) { .limit(10) // 최대 10개의 데이터 가져오기 ).items().stream().toList(); } + + /*public PaginatedResult findByChatRoomIdWithPagination(Long chatRoomId, LocalDateTime lastEvaluatedKey, int pageSize) { + QueryEnhancedRequest.Builder queryBuilder = QueryEnhancedRequest.builder() + .queryConditional(QueryConditional.keyEqualTo(Key.builder().partitionValue(chatRoomId).build())) + .limit(pageSize) // 한 번에 가져올 메시지 개수 제한 + .scanIndexForward(false); // 최신 메시지부터 정렬 (내림차순) + + // 마지막으로 가져온 메시지가 있으면 ExclusiveStartKey 설정 + if (lastEvaluatedKey != null) { + queryBuilder.exclusiveStartKey(Map.of( + "chatroom_id", AttributeValue.builder().n(String.valueOf(chatRoomId)).build(), + "created_at", AttributeValue.builder().s(lastEvaluatedKey.toString()).build() + )); + } + + // 쿼리 실행 + PageIterable results = chatMessageTable.query(queryBuilder.build()); + + // 첫 번째 페이지 가져오기 + Iterator> iterator = results.iterator(); + if (!iterator.hasNext()) { + return new PaginatedResult<>(Collections.emptyList(), null); + } + + Page page = iterator.next(); + List messages = page.items(); + + // 다음 페이지 조회를 위한 Key 설정 + Map lastKeyMap = page.lastEvaluatedKey(); + LocalDateTime nextLastKey = null; + if (lastKeyMap != null && lastKeyMap.containsKey("created_at")) { + nextLastKey = LocalDateTime.parse(lastKeyMap.get("created_at").s()); + } + + return new PaginatedResult<>(messages, nextLastKey); + } + + @Getter + @AllArgsConstructor + public class PaginatedResult { + private List items; // 현재 페이지의 메시지 리스트 + private LocalDateTime lastEvaluatedKey; // 다음 페이지 조회를 위한 키 + + *//* + 1. 첫 페이지 요청 + lastEvaluatedKey = null 로 요청 + 최신 메시지 pageSize개 조회 + 2. 스크롤하여 추가 데이터 요청 + 마지막으로 가져온 lastEvaluatedKey 값 전달 + 3. 더 이상 데이터 없으면 lastEvaluated는 null + 클라이언트는 추가 요청 중단 + *//* + }*/ + + /*// 여러 chatRoomId에 대해 배치 조회 (BatchGetItem 사용) + public List findByChatRoomIds(List chatRoomIds) { + // BatchGetItem 요청 생성 + List> keys = new ArrayList<>(); + + chatRoomIds.forEach(chatRoomId -> { + Map key = new HashMap<>(); + key.put("chatRoomId", AttributeValue.builder().n(chatRoomId.toString()).build()); + keys.add(key); + }); + + // KeysAndAttributes에 keys 추가 + Map requestItems = new HashMap<>(); + requestItems.put("chat_messages", KeysAndAttributes.builder().keys(keys).build()); + + BatchGetItemRequest batchGetItemRequest = BatchGetItemRequest.builder() + .requestItems(requestItems) + .build(); + + // BatchGetItemResponse로 변경 + BatchGetItemResponse result = dynamoDbEnhancedClient.batchGetItem(batchGetItemRequest); + + return result.responses().get("chat_messages").stream() + .map(item -> convertToChatMessage(item)) + .collect(Collectors.toList()); + } + + // 응답을 ChatMessage 객체로 변환하는 메서드 (필요한 변환 로직 추가) + private ChatMessage convertToChatMessage(Map item) { + // 변환 로직 추가 + return new ChatMessage(); // 적절한 변환 로직을 작성하세요 + }*/ } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index f635b9a..85126a0 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -1,8 +1,8 @@ package com.rljj.chatservice.domain.chat.service; -import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; -import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; -import com.rljj.chatservice.domain.chat.dto.Message; +import com.rljj.chatservice.domain.chat.dto.*; +import com.rljj.chatservice.domain.chat.model.ChatMessage; +import com.rljj.chatservice.domain.chat.respository.ChatMessageRepository; import com.rljj.chatservice.domain.chat.respository.ChatRoomRepository; import com.rljj.switchswitchentity.chat.ChatRoom; import lombok.RequiredArgsConstructor; @@ -20,11 +20,12 @@ public class ChatService { private final MessageSender sender; private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; // 채팅방 만들기 @Transactional public void makeChatRoom(Long memberId, ChatRoomRequest requestDto) { - // 1. 유효성 체크 + // 1. 유효성 체크 => feignClient // 2. 저장 chatRoomRepository.save(requestDto.toEntity(memberId)); @@ -45,12 +46,17 @@ public List getChatRoomList(Long memberId) { )) .toList(); - // TODO 다이나모DB 조회해서 마지막 메시지 resposne에 넣어줘야 함 + // TODO 다이나모 DB 조회해서 마지막 메시지 resposne에 넣어줘야 함 return chatRoomResponseList; } + // 채팅메시지 내역 조회하기 + public ChatMessageListResponse getChatMessageList(Long roomId, Long memberId) { + List chatMessageList = chatMessageRepository.findByChatRoomId(roomId); + return new ChatMessageListResponse(ChatMessageDto.from(chatMessageList, memberId)); + } - + // 채팅메시지 보내기 public void sendMessage(Message message) { // 1. accessToken으로 member 찾고 @@ -60,5 +66,5 @@ public void sendMessage(Message message) { // 3. 메시지를 전송한다. sender.send("chat", message); // TOPIC: chat 임시 설정 } - + } From 628f84d1a547ee59d149b9aa1c7da5fcdb8e4473 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Mon, 10 Feb 2025 10:39:43 +0900 Subject: [PATCH 04/11] Refactoring/rename-Member-related-columns (#65) - creatorUser -> author, interestedUser -> requester --- .../domain/chat/respository/ChatRoomRepository.java | 6 +++--- .../chatservice/domain/chat/service/ChatService.java | 6 +++--- .../com/rljj/switchswitchentity/chat/ChatRoom.java | 10 ++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java index aa5884a..8020888 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/respository/ChatRoomRepository.java @@ -12,10 +12,10 @@ public interface ChatRoomRepository extends JpaRepository { @Query("SELECT c FROM ChatRoom c " + - "JOIN FETCH c.creatorUser " + - "JOIN FETCH c.interestedUser " + + "JOIN FETCH c.author " + + "JOIN FETCH c.requester " + "JOIN FETCH c.chipPost " + - "WHERE c.creatorUser.id = :memberId OR c.interestedUser.id = :memberId") + "WHERE c.author.id = :memberId OR c.requester.id = :memberId") List findChatRoomsByMemberId(@Param("memberId") Long memberId); } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index 85126a0..2522543 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -39,9 +39,9 @@ public List getChatRoomList(Long memberId) { .map(chatRoom -> new ChatRoomResponse( chatRoom.getId(), memberId, // 현재 로그인한 사용자 ID - chatRoom.getCreatorUser().getId().equals(memberId) - ? chatRoom.getInterestedUser().getId() // 내가 creatorUser면 상대방 = interestedUser - : chatRoom.getCreatorUser().getId(), // 내가 interestedUser면 상대방 = creatorUser + chatRoom.getAuthor().getId().equals(memberId) + ? chatRoom.getRequester().getId() // 내가 author 상대방 = requester + : chatRoom.getAuthor().getId(), // 내가 requester 상대방 = author chatRoom.getChipPost().getId() )) .toList(); diff --git a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java index 3c3cb8a..f76e82a 100644 --- a/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java +++ b/switchswitch-entity/src/main/java/com/rljj/switchswitchentity/chat/ChatRoom.java @@ -18,15 +18,13 @@ public class ChatRoom extends BaseEntity { @NotNull private ChipPost chipPost; - // TODO creatorUser -> author, @ManyToOne(fetch = FetchType.LAZY) @NotNull - private Member creatorUser; + private Member author; - // TODO interestedUser -> requester로 이름 바꾸기 @ManyToOne(fetch = FetchType.LAZY) @NotNull - private Member interestedUser; + private Member requester; @Enumerated(EnumType.STRING) @NotNull @@ -39,8 +37,8 @@ public static ChatRoom create(ChipPost chipPost, Member author, Member requester @Builder private ChatRoom(ChipPost chipPost, Member author, Member requester) { this.chipPost = chipPost; - this.creatorUser = author; - this.interestedUser = requester; + this.author = author; + this.requester = requester; this.status = ChatRoomStatus.ACTIVE; } From 51ceb87a0719a811f8dd1bf91844e2537de598bd Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Mon, 10 Feb 2025 16:04:28 +0900 Subject: [PATCH 05/11] Feature/add-security-config (#65, #74) --- chat-service/build.gradle | 2 + .../chat/controller/ChatController.java | 21 ++++----- .../global/config/jwt/JwtConfig.java | 25 +++++++++++ .../config/security/SecurityConfig.java | 43 +++++++++++++++++++ .../global/util/SecurityUtils.java | 31 +++++++++++++ 5 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/util/SecurityUtils.java diff --git a/chat-service/build.gradle b/chat-service/build.gradle index 0a563ea..c74cb18 100644 --- a/chat-service/build.gradle +++ b/chat-service/build.gradle @@ -1,5 +1,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' @@ -17,5 +18,6 @@ dependencies { runtimeOnly 'org.postgresql:postgresql:42.7.5' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } \ No newline at end of file diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 8e72cac..325103c 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -2,6 +2,7 @@ import com.rljj.chatservice.domain.chat.dto.*; import com.rljj.chatservice.domain.chat.service.ChatService; +import com.rljj.chatservice.global.util.SecurityUtils; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -13,24 +14,20 @@ @RestController @RequiredArgsConstructor public class ChatController { + private final ChatService chatService; - // TODO 파라미터 memberId 임시 -> security로 처리할 것 // 채팅방 만들기 @PostMapping("/rooms") - public ResponseEntity createChatRoom( - @RequestParam Long memberId, - @RequestBody ChatRoomRequest request) { - chatService.makeChatRoom(memberId, request); + public ResponseEntity createChatRoom(@RequestBody ChatRoomRequest request) { + chatService.makeChatRoom(SecurityUtils.getMemberId(), request); return ResponseEntity.ok().build(); } - // TODO 일단 all 조회로 만들기 -> 페이징 처리 어캐 할지 // 채팅방 리스트 조회하기 @GetMapping("/rooms") - public ResponseEntity> getChatRoomList( - @RequestParam Long memberId) { - List chatRoomList = chatService.getChatRoomList(memberId); + public ResponseEntity> getChatRoomList() { + List chatRoomList = chatService.getChatRoomList(SecurityUtils.getMemberId()); return ResponseEntity.ok(chatRoomList); } @@ -39,10 +36,8 @@ public ResponseEntity> getChatRoomList( // 채팅메시지 내역 조회하기 @GetMapping("/rooms/{roomId}") - public ResponseEntity getChatMessageList( - @PathVariable("roomId") Long roomId, - @RequestParam Long memberId) { - ChatMessageListResponse chatMessageList = chatService.getChatMessageList(roomId, memberId); + public ResponseEntity getChatMessageList(@PathVariable("roomId") Long roomId) { + ChatMessageListResponse chatMessageList = chatService.getChatMessageList(roomId, SecurityUtils.getMemberId()); return ResponseEntity.ok(chatMessageList); } diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java new file mode 100644 index 0000000..8252c72 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java @@ -0,0 +1,25 @@ +package com.rljj.chatservice.global.config.jwt; + +import com.rljj.switchswitchcommon.jwt.JwtProvider; +import com.rljj.switchswitchcommon.jwt.JwtProviderImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JwtConfig { + + @Value("${jwt.expired.access-token}") + private long accessTokenExpireTime; + + @Value("${jwt.expired.refresh-token}") + private long refreshTokenExpireTime; + + @Value("${jwt.secret}") + private String jwtSecret; + + @Bean + public JwtProvider jwtProvider() { + return new JwtProviderImpl(accessTokenExpireTime, refreshTokenExpireTime, jwtSecret); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java new file mode 100644 index 0000000..db81300 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.rljj.chatservice.global.config.security; + +import com.rljj.switchswitchcommon.jwt.JwtAuthenticationFilter; +import com.rljj.switchswitchcommon.jwt.JwtProvider; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtProvider jwtProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSession httpSession) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/api/chat/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) + .exceptionHandling( + exception -> exception.accessDeniedPage("/login?error=403") + ) + ; + return http.build(); + } + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(jwtProvider); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/util/SecurityUtils.java b/chat-service/src/main/java/com/rljj/chatservice/global/util/SecurityUtils.java new file mode 100644 index 0000000..b420e98 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/util/SecurityUtils.java @@ -0,0 +1,31 @@ +package com.rljj.chatservice.global.util; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public abstract class SecurityUtils { + // 인스턴스화 방지를 위한 private 생성자 추가 + private SecurityUtils() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static String getUsername() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof UserDetails) { + return ((UserDetails) principal).getUsername(); + } + return null; + } + + public static Long getMemberId() { + String username = getUsername(); + if (username != null) { + try { + return Long.parseLong(username); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Username is not a valid number: " + username, e); + } + } + throw new IllegalStateException("Username is null"); + } +} From 2daa243eadc521b597ab4e1fffe7d3a8c28c7e3d Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Tue, 11 Feb 2025 13:52:00 +0900 Subject: [PATCH 06/11] Feature/implement-stomphandler (#65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CONNECT 시 인터셉터로 토큰 검증 - redis config 추가 --- chat-service/build.gradle | 5 ++ .../global/config/jwt/JwtConfig.java | 8 ++ .../global/config/redis/RedisConfig.java | 31 +++++++ .../global/config/websocket/StompHandler.java | 84 +++++++++++++++++++ ...onfiguration.java => WebSocketConfig.java} | 11 ++- 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/config/redis/RedisConfig.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java rename chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/{WebSocketConfiguration.java => WebSocketConfig.java} (85%) diff --git a/chat-service/build.gradle b/chat-service/build.gradle index c74cb18..692935e 100644 --- a/chat-service/build.gradle +++ b/chat-service/build.gradle @@ -3,6 +3,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-config-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' @@ -17,6 +20,8 @@ dependencies { testImplementation 'org.springframework.kafka:spring-kafka-test' runtimeOnly 'org.postgresql:postgresql:42.7.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java index 8252c72..a424fdc 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/jwt/JwtConfig.java @@ -2,9 +2,12 @@ import com.rljj.switchswitchcommon.jwt.JwtProvider; import com.rljj.switchswitchcommon.jwt.JwtProviderImpl; +import com.rljj.switchswitchcommon.jwt.JwtRedisService; +import com.rljj.switchswitchcommon.jwt.JwtRedisServiceImpl; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class JwtConfig { @@ -22,4 +25,9 @@ public class JwtConfig { public JwtProvider jwtProvider() { return new JwtProviderImpl(accessTokenExpireTime, refreshTokenExpireTime, jwtSecret); } + + @Bean + public JwtRedisService jwtRedisService(StringRedisTemplate redisTemplate, JwtProvider jwtProvider) { + return new JwtRedisServiceImpl(redisTemplate, jwtProvider); + } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/redis/RedisConfig.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/redis/RedisConfig.java new file mode 100644 index 0000000..78e05a2 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/redis/RedisConfig.java @@ -0,0 +1,31 @@ +package com.rljj.chatservice.global.config.redis; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); + } + + @Bean + public StringRedisTemplate redisTemplate() { + StringRedisTemplate redisTemplate = new StringRedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java new file mode 100644 index 0000000..6b6f908 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java @@ -0,0 +1,84 @@ +package com.rljj.chatservice.global.config.websocket; + +import com.rljj.switchswitchcommon.jwt.JwtProvider; +import com.rljj.switchswitchcommon.jwt.JwtRedisService; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +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.security.access.AccessDeniedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Order(Ordered.HIGHEST_PRECEDENCE + 99) // 우선 순위를 높게 설정해서 Security filter들 보다 앞서 실행되게 해준다. +@Component +@RequiredArgsConstructor +@Slf4j +public class StompHandler implements ChannelInterceptor { + private final JwtProvider jwtProvider; + private final JwtRedisService jwtRedisService; + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + + if (accessor.getCommand() == StompCommand.CONNECT) { + String token = extractToken(accessor); + + // 토큰이 없거나 차단된 경우 접근 거부 + if (isTokenInvalidOrBlocked(token)) { + throw new AccessDeniedException("Access denied: Invalid or blocked token."); + } + + // JWT 검증 + authenticateToken(token, accessor); + } + + log.info("StompAccessor = {}", accessor); + return message; + } + + private String extractToken(StompHeaderAccessor accessor) { + List authHeaders = accessor.getNativeHeader("Authorization"); + if (authHeaders != null && !authHeaders.isEmpty()) { + String token = authHeaders.get(0); + return token.startsWith("Bearer ") ? token.substring(7) : token; + } + return null; + } + + private boolean isTokenInvalidOrBlocked(String token) { + return token == null || jwtRedisService.isBlockedAccessToken(token); + } + + private void authenticateToken(String token, StompHeaderAccessor accessor) { + try { + jwtProvider.validateJwt(token); + } catch (ExpiredJwtException e) { + throw new AccessDeniedException("Access denied: Token has expired."); + } catch (Exception e) { + throw new AccessDeniedException("Access denied: Invalid token."); + } + + Long memberId = jwtProvider.parseMemberId(token); + UserDetails userDetails = createUserDetails(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + accessor.setUser(authentication); + } + + private UserDetails createUserDetails(Long memberId) { + return new User(String.valueOf(memberId), "", List.of(new SimpleGrantedAuthority("ROLE_USER"))); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfig.java similarity index 85% rename from chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java rename to chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfig.java index 77cee9e..6d089b8 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfiguration.java +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/WebSocketConfig.java @@ -1,6 +1,8 @@ package com.rljj.chatservice.global.config.websocket; +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; @@ -8,8 +10,11 @@ import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; @Configuration +@RequiredArgsConstructor @EnableWebSocketMessageBroker -public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompHandler stompHandler; // STOMP 엔드포인트를 등록하는 메서드 @Override @@ -26,12 +31,12 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/pub"); // /pub/message로 메시지 전송 컨트롤러 라우팅 가능: 메시지 수신 } -/* // 클라이언트 인바운드 채널을 구성하는 메서드 + // 클라이언트 인바운드 채널을 구성하는 메서드 @Override public void configureClientInboundChannel(ChannelRegistration registration) { // stompHandler를 인터셉터로 등록하여 STOMP 메시지 핸들링을 수행 registration.interceptors(stompHandler); - }*/ + } // STOMP에서 64KB 이상의 데이터 전송을 못하는 문제 해결 @Override From b2ee3c243da1369b33a2288d4e4afd2e6de7c720 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Tue, 11 Feb 2025 14:08:02 +0900 Subject: [PATCH 07/11] Feature/update-send-message (#65) --- .../chat/controller/ChatController.java | 14 +++++--- .../chatservice/domain/chat/dto/Message.java | 32 +++++++++++++++-- .../domain/chat/service/ChatService.java | 22 ++++++++---- .../domain/chat/service/MessageReceiver.java | 7 ++-- .../global/util/ConstantUtils.java | 5 +++ chat-service/src/main/resources/test.html | 34 ++++++++++++------- 6 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/global/util/ConstantUtils.java diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 325103c..6d5b9e5 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -1,11 +1,15 @@ package com.rljj.chatservice.domain.chat.controller; -import com.rljj.chatservice.domain.chat.dto.*; +import com.rljj.chatservice.domain.chat.dto.ChatMessageListResponse; +import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; +import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; +import com.rljj.chatservice.domain.chat.dto.Message; import com.rljj.chatservice.domain.chat.service.ChatService; import com.rljj.chatservice.global.util.SecurityUtils; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -14,7 +18,7 @@ @RestController @RequiredArgsConstructor public class ChatController { - + private final ChatService chatService; // 채팅방 만들기 @@ -24,6 +28,7 @@ public ResponseEntity createChatRoom(@RequestBody ChatRoomRequest request) return ResponseEntity.ok().build(); } + // TODO 응답 정리하기 // 채팅방 리스트 조회하기 @GetMapping("/rooms") public ResponseEntity> getChatRoomList() { @@ -32,7 +37,6 @@ public ResponseEntity> getChatRoomList() { } // TODO 채팅방 삭제하기 - // TODO 채팅방 접속 끊기 // 채팅메시지 내역 조회하기 @GetMapping("/rooms/{roomId}") @@ -43,8 +47,8 @@ public ResponseEntity getChatMessageList(@PathVariable( // 채팅메시지 보내기 @MessageMapping("/messages") - public void sendMessage(Message message) { - chatService.sendMessage(message); + public void sendMessage(Message message, Authentication authentication) { + chatService.sendMessage(message, authentication); } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java index 9151fc9..273ad93 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/Message.java @@ -1,7 +1,11 @@ package com.rljj.chatservice.domain.chat.dto; +import com.rljj.chatservice.domain.chat.model.ChatMessage; import lombok.*; + import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; @Getter @Builder @@ -9,8 +13,32 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Message implements Serializable { private String id; - private Integer chatNo; - private String content; + private Long chatRoomId; + private String message; + + private Long senderId; + //private Long senderNickname; + + private LocalDateTime createdAt; + + // Method to set id, senderId, and createdAt at once + public void setMessageDetails(Long senderId, LocalDateTime createdAt) { + this.id = UUID.randomUUID().toString(); + this.senderId = senderId; + this.createdAt = createdAt; + } + + // Message를 ChatMessage로 변환하는 메서드 + public ChatMessage toChatMessage() { + return ChatMessage.builder() + .chatRoomId(this.chatRoomId) + .createdAt(this.createdAt) + .messageId(this.id) + .senderId(this.senderId) + .message(this.message) + .build(); + } + } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index 2522543..14b962f 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -4,12 +4,16 @@ import com.rljj.chatservice.domain.chat.model.ChatMessage; import com.rljj.chatservice.domain.chat.respository.ChatMessageRepository; import com.rljj.chatservice.domain.chat.respository.ChatRoomRepository; +import com.rljj.chatservice.global.util.ConstantUtils; import com.rljj.switchswitchentity.chat.ChatRoom; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; @Slf4j @@ -57,14 +61,20 @@ public ChatMessageListResponse getChatMessageList(Long roomId, Long memberId) { } // 채팅메시지 보내기 - public void sendMessage(Message message) { - // 1. accessToken으로 member 찾고 + public void sendMessage(Message message, Authentication authentication) { - // 2. message 객체에 보낸시간, 보낸사람 memberNo, 닉네임을 셋팅해준다. - //message.setSendTimeAndSender(LocalDateTime.now(), findMember.getMemberNo(), findMember.getNickname(), readCount); + // 1. message 객체에 필요한 정보 세팅 + message.setMessageDetails(extractMemberId(authentication), LocalDateTime.now()); - // 3. 메시지를 전송한다. - sender.send("chat", message); // TOPIC: chat 임시 설정 + // 2. 메시지 전송 + sender.send(ConstantUtils.KAFKA_TOPIC, message); + + // 3. dynamodb 저장 + chatMessageRepository.saveChatMessage(message.toChatMessage()); + } + + private Long extractMemberId(Authentication authentication) { + return Long.parseLong(((UserDetails) authentication.getPrincipal()).getUsername()); } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java index e28e471..c476ad7 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/MessageReceiver.java @@ -1,6 +1,7 @@ package com.rljj.chatservice.domain.chat.service; import com.rljj.chatservice.domain.chat.dto.Message; +import com.rljj.chatservice.global.util.ConstantUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; @@ -13,12 +14,12 @@ public class MessageReceiver { private final SimpMessageSendingOperations template; - @KafkaListener(topics = "chat", autoStartup = "true") + @KafkaListener(topics = ConstantUtils.KAFKA_TOPIC, autoStartup = "true") public void receiveMessage(Message message) { - log.info("전송 위치 = /sub/public/"+ message.getChatNo()); + log.info("전송 위치 = /sub/public/"+ message.getChatRoomId()); log.info("채팅 방으로 메시지 전송 = {}", message); // 메시지객체 내부의 채팅방번호를 참조하여, 해당 채팅방 구독자에게 메시지를 발송한다. - template.convertAndSend("/sub/public/" + message.getChatNo(), message); + template.convertAndSend("/sub/public/" + message.getChatRoomId(), message); } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/util/ConstantUtils.java b/chat-service/src/main/java/com/rljj/chatservice/global/util/ConstantUtils.java new file mode 100644 index 0000000..46180e4 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/global/util/ConstantUtils.java @@ -0,0 +1,5 @@ +package com.rljj.chatservice.global.util; + +public abstract class ConstantUtils { + public static final String KAFKA_TOPIC = "chat"; +} diff --git a/chat-service/src/main/resources/test.html b/chat-service/src/main/resources/test.html index aa13189..307aff8 100644 --- a/chat-service/src/main/resources/test.html +++ b/chat-service/src/main/resources/test.html @@ -13,24 +13,32 @@

WebSocket 테스트

From 82056f3ebfed700f81d0a2a0a5f88839ff3a2eb1 Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Tue, 11 Feb 2025 14:24:17 +0900 Subject: [PATCH 08/11] Refactoring/update-chatroomlist-response (#65) --- .../chat/controller/ChatController.java | 10 +++--- .../domain/chat/dto/ChatRoomDto.java | 34 +++++++++++++++++++ .../domain/chat/dto/ChatRoomListResponse.java | 12 +++++++ .../domain/chat/dto/ChatRoomResponse.java | 16 --------- .../domain/chat/service/ChatService.java | 17 ++-------- 5 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomDto.java create mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomListResponse.java delete mode 100644 chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 6d5b9e5..82473b2 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -1,8 +1,8 @@ package com.rljj.chatservice.domain.chat.controller; import com.rljj.chatservice.domain.chat.dto.ChatMessageListResponse; +import com.rljj.chatservice.domain.chat.dto.ChatRoomListResponse; import com.rljj.chatservice.domain.chat.dto.ChatRoomRequest; -import com.rljj.chatservice.domain.chat.dto.ChatRoomResponse; import com.rljj.chatservice.domain.chat.dto.Message; import com.rljj.chatservice.domain.chat.service.ChatService; import com.rljj.chatservice.global.util.SecurityUtils; @@ -12,8 +12,6 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RequestMapping("/api/chat") @RestController @RequiredArgsConstructor @@ -28,15 +26,15 @@ public ResponseEntity createChatRoom(@RequestBody ChatRoomRequest request) return ResponseEntity.ok().build(); } - // TODO 응답 정리하기 // 채팅방 리스트 조회하기 @GetMapping("/rooms") - public ResponseEntity> getChatRoomList() { - List chatRoomList = chatService.getChatRoomList(SecurityUtils.getMemberId()); + public ResponseEntity getChatRoomList() { + ChatRoomListResponse chatRoomList = chatService.getChatRoomList(SecurityUtils.getMemberId()); return ResponseEntity.ok(chatRoomList); } // TODO 채팅방 삭제하기 + // TODO 채팅방 및 메시지 내역 페이징 방법 정하기 // 채팅메시지 내역 조회하기 @GetMapping("/rooms/{roomId}") diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomDto.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomDto.java new file mode 100644 index 0000000..d0bd4f7 --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomDto.java @@ -0,0 +1,34 @@ +package com.rljj.chatservice.domain.chat.dto; + +import com.rljj.switchswitchentity.chat.ChatRoom; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class ChatRoomDto { + + private Long chatRoomId; + private Long memberId; // 현재 멤버 ID + private Long chatPartnerId; // 채팅 상대방 ID + private Long chipPostId; // 게시물 ID +// private String latestMessage; +// private LocalDateTime latestMessageCreatedAt; + + public ChatRoomDto(ChatRoom chatRoom, Long memberId) { + this.chatRoomId = chatRoom.getId(); + this.memberId = memberId; + this.chatPartnerId = chatRoom.getAuthor().getId().equals(memberId) + ? chatRoom.getRequester().getId() // 내가 author 상대방 = requester + : chatRoom.getAuthor().getId(); + this.chipPostId = chatRoom.getChipPost().getId(); + } + + public static List from(List chatRoomList, Long memberId) { + return chatRoomList.stream() + .map(chatRoom -> new ChatRoomDto(chatRoom, memberId)) + .toList(); + } +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomListResponse.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomListResponse.java new file mode 100644 index 0000000..f30d8fe --- /dev/null +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomListResponse.java @@ -0,0 +1,12 @@ +package com.rljj.chatservice.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class ChatRoomListResponse { + private List rooms; +} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java deleted file mode 100644 index 9557365..0000000 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.rljj.chatservice.domain.chat.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class ChatRoomResponse { - - private Long chatRoomId; - private Long memberId; // 현재 멤버 ID - private Long chatPartnerId; // 채팅 상대방 ID - private Long chipPostId; // 게시물 ID -// private String latestMessage; -// private LocalDateTime latestMessageCreatedAt; -} diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index 14b962f..970de5c 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -36,22 +36,9 @@ public void makeChatRoom(Long memberId, ChatRoomRequest requestDto) { } // 채팅방 리스트 조회 - public List getChatRoomList(Long memberId) { + public ChatRoomListResponse getChatRoomList(Long memberId) { List chatRoomList = chatRoomRepository.findChatRoomsByMemberId(memberId); - - List chatRoomResponseList = chatRoomList.stream() - .map(chatRoom -> new ChatRoomResponse( - chatRoom.getId(), - memberId, // 현재 로그인한 사용자 ID - chatRoom.getAuthor().getId().equals(memberId) - ? chatRoom.getRequester().getId() // 내가 author 상대방 = requester - : chatRoom.getAuthor().getId(), // 내가 requester 상대방 = author - chatRoom.getChipPost().getId() - )) - .toList(); - - // TODO 다이나모 DB 조회해서 마지막 메시지 resposne에 넣어줘야 함 - return chatRoomResponseList; + return new ChatRoomListResponse(ChatRoomDto.from(chatRoomList, memberId)); } // 채팅메시지 내역 조회하기 From 361b88d449e1395ce8713a46780231bd74255ffd Mon Sep 17 00:00:00 2001 From: "DESKTOP-93CFOT8\\hjryu" Date: Tue, 11 Feb 2025 17:37:55 +0900 Subject: [PATCH 09/11] BugFix/add-headers-to-STOMP-message-send-logic (#65) --- .../chat/controller/ChatController.java | 11 +++++--- .../domain/chat/dto/ChatRoomRequest.java | 6 +++++ .../domain/chat/service/ChatService.java | 27 ++++++++++++------- .../config/security/SecurityConfig.java | 2 +- .../global/config/websocket/StompHandler.java | 13 --------- chat-service/src/main/resources/test.html | 13 ++++++--- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java index 82473b2..04c96fd 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/controller/ChatController.java @@ -6,12 +6,15 @@ import com.rljj.chatservice.domain.chat.dto.Message; import com.rljj.chatservice.domain.chat.service.ChatService; import com.rljj.chatservice.global.util.SecurityUtils; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +@Slf4j @RequestMapping("/api/chat") @RestController @RequiredArgsConstructor @@ -21,7 +24,7 @@ public class ChatController { // 채팅방 만들기 @PostMapping("/rooms") - public ResponseEntity createChatRoom(@RequestBody ChatRoomRequest request) { + public ResponseEntity createChatRoom(@RequestBody @Valid final ChatRoomRequest request) { chatService.makeChatRoom(SecurityUtils.getMemberId(), request); return ResponseEntity.ok().build(); } @@ -45,8 +48,8 @@ public ResponseEntity getChatMessageList(@PathVariable( // 채팅메시지 보내기 @MessageMapping("/messages") - public void sendMessage(Message message, Authentication authentication) { - chatService.sendMessage(message, authentication); + public void sendMessage(Message message, @Header("Authorization") final String accessToken) { + chatService.sendMessage(message, accessToken); } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java index bb7d18d..908eebd 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/dto/ChatRoomRequest.java @@ -3,13 +3,19 @@ import com.rljj.switchswitchentity.chat.ChatRoom; import com.rljj.switchswitchentity.chip.chippost.ChipPost; import com.rljj.switchswitchentity.member.Member; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @AllArgsConstructor +@NoArgsConstructor public class ChatRoomRequest { + + @NotNull private Long chipPostId; // 게시물 id + @NotNull private Long requesterId; // 거래요청건 멤버 id public ChatRoom toEntity(Long memberId) { diff --git a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java index 970de5c..75cfc16 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java +++ b/chat-service/src/main/java/com/rljj/chatservice/domain/chat/service/ChatService.java @@ -5,11 +5,10 @@ import com.rljj.chatservice.domain.chat.respository.ChatMessageRepository; import com.rljj.chatservice.domain.chat.respository.ChatRoomRepository; import com.rljj.chatservice.global.util.ConstantUtils; +import com.rljj.switchswitchcommon.jwt.JwtProvider; import com.rljj.switchswitchentity.chat.ChatRoom; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +21,7 @@ @RequiredArgsConstructor public class ChatService { + private final JwtProvider jwtProvider; private final MessageSender sender; private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; @@ -48,20 +48,29 @@ public ChatMessageListResponse getChatMessageList(Long roomId, Long memberId) { } // 채팅메시지 보내기 - public void sendMessage(Message message, Authentication authentication) { + public void sendMessage(Message message, String accessToken) { - // 1. message 객체에 필요한 정보 세팅 - message.setMessageDetails(extractMemberId(authentication), LocalDateTime.now()); + // 1. 토큰에서 memberId 추출 + Long memberId = extractMemberIdFromToken(accessToken); + if (memberId == null) { + throw new IllegalArgumentException("Invalid or missing access token"); + } - // 2. 메시지 전송 + // 2. message 객체에 필요한 정보 세팅 + message.setMessageDetails(memberId, LocalDateTime.now()); + + // 3. 메시지 전송 sender.send(ConstantUtils.KAFKA_TOPIC, message); - // 3. dynamodb 저장 + // 4. dynamodb 저장 chatMessageRepository.saveChatMessage(message.toChatMessage()); } - private Long extractMemberId(Authentication authentication) { - return Long.parseLong(((UserDetails) authentication.getPrincipal()).getUsername()); + private Long extractMemberIdFromToken(String token) { + if (token != null && token.startsWith("Bearer ")) { + return jwtProvider.parseMemberId(token.substring(7)); + } + return null; } } diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java index db81300..a4e53fa 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/security/SecurityConfig.java @@ -25,7 +25,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSession ht http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/chat/**").permitAll() + .requestMatchers("/chat/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java index 6b6f908..dce5a94 100644 --- a/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java +++ b/chat-service/src/main/java/com/rljj/chatservice/global/config/websocket/StompHandler.java @@ -13,11 +13,6 @@ import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.List; @@ -71,14 +66,6 @@ private void authenticateToken(String token, StompHeaderAccessor accessor) { } catch (Exception e) { throw new AccessDeniedException("Access denied: Invalid token."); } - - Long memberId = jwtProvider.parseMemberId(token); - UserDetails userDetails = createUserDetails(memberId); - Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - accessor.setUser(authentication); } - private UserDetails createUserDetails(Long memberId) { - return new User(String.valueOf(memberId), "", List.of(new SimpleGrantedAuthority("ROLE_USER"))); - } } diff --git a/chat-service/src/main/resources/test.html b/chat-service/src/main/resources/test.html index 307aff8..d1c3c27 100644 --- a/chat-service/src/main/resources/test.html +++ b/chat-service/src/main/resources/test.html @@ -14,7 +14,7 @@

WebSocket 테스트