diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java deleted file mode 100644 index 56da642..0000000 --- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.assu.server.domain.certification.config; - -import org.springframework.context.annotation.Bean; -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.WebSocketHandler; -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 lombok.RequiredArgsConstructor; - -// @EnableWebSocketMessageBroker -// @Configuration -// @RequiredArgsConstructor -// public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer { -// -// private final StompAuthChannelInterceptor stompAuthChannelInterceptor; -// @Override -// public void configureMessageBroker(MessageBrokerRegistry config) { -// config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소 -// config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소 -// } -// -// @Override -// public void registerStompEndpoints(StompEndpointRegistry registry) { -// registry.addEndpoint("/ws-certify").setAllowedOriginPatterns("*"); // 클라이언트 WebSocket 연결 주소 -// // .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용 -// } -// -// @Override -// public void configureClientInboundChannel(ChannelRegistration registration) { -// registration.interceptors(stompAuthChannelInterceptor); -// } -// -// } diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java deleted file mode 100644 index 8026ce3..0000000 --- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java +++ /dev/null @@ -1,93 +0,0 @@ -// package com.assu.server.domain.chat.config; -// -// 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.*; -// -// @Configuration -// @EnableWebSocketMessageBroker -// public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { -// -// @Override -// public void registerStompEndpoints(StompEndpointRegistry registry) { -// registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 지점 -// .setAllowedOriginPatterns( -// "*", -// "https://assu.shop", -// "http://localhost:63342", -// "http://localhost:5173", // Vite 기본 -// "http://localhost:3000", // CRA/Next 기본 -// "http://127.0.0.1:*", -// "http://192.168.*.*:*"); // 같은 LAN의 실제 기기 테스트용// fallback for old browsers -// // 같은 LAN의 실제 기기 테스트용 -// // fallback for old browsers -// -// // ✅ 모바일/안드로이드용 (네이티브 WebSocket) -// registry.addEndpoint("/ws") -// .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅 -// } -// -// @Override -// public void configureMessageBroker(MessageBrokerRegistry registry) { -// registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix -// registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix -// } -// } -package com.assu.server.domain.chat.config; - -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; - -import com.assu.server.domain.certification.config.StompAuthChannelInterceptor; - -import lombok.RequiredArgsConstructor; - -@Configuration -@EnableWebSocketMessageBroker -@RequiredArgsConstructor -public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - private final StompAuthChannelInterceptor stompAuthChannelInterceptor; - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - - registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 지점 - .setAllowedOriginPatterns( - "*", - "https://assu.shop", - "http://localhost:63342", - "http://localhost:5173", // Vite 기본 - "http://localhost:3000", // CRA/Next 기본 - "http://127.0.0.1:*", - "http://192.168.*.*:*"); - // 채팅용 엔드포인트 - - // 인증용 엔드포인트 - registry.addEndpoint("/ws-certify") - .setAllowedOriginPatterns("*"); - - - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - // 채팅용 - registry.setApplicationDestinationPrefixes("/pub"); - registry.enableSimpleBroker("/sub"); - - // 인증용 추가 - registry.setApplicationDestinationPrefixes("/pub", "/app"); // 둘 다 추가 - registry.enableSimpleBroker("/sub", "/certification"); // 둘 다 추가 - } - - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(stompAuthChannelInterceptor); - } -} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 53af1a4..7243da9 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -1,23 +1,20 @@ package com.assu.server.domain.chat.controller; import com.assu.server.domain.chat.dto.*; -import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.chat.redis.RedisPublisher; import com.assu.server.domain.chat.service.BlockService; import com.assu.server.domain.chat.service.ChatService; -import com.assu.server.domain.member.repository.MemberRepository; -import com.assu.server.domain.notification.service.NotificationCommandService; +import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; -import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import com.assu.server.global.apiPayload.BaseResponse; -import org.springframework.messaging.simp.SimpMessagingTemplate; import java.util.List; @@ -28,16 +25,13 @@ public class ChatController { private final ChatService chatService; private final SimpMessagingTemplate simpMessagingTemplate; -// private final PresenceTracker presenceTracker; -// private final MessageRepository messageRepository; -// private final MemberRepository memberRepository; private final BlockService blockService; -// private final NotificationCommandService notificationCommandService; + private final RedisPublisher redisPublisher; @Operation( summary = "채팅방을 생성하는 API", description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed80c38871ec77deced713) 채팅방을 생성합니다.\n"+ - "- storeId: Request Body, Long\n" + + "- adminId: Request Body, Long\n" + "- partnerId: Request Body, Long\n" ) @PostMapping("/rooms") @@ -53,7 +47,7 @@ public BaseResponse createChatRoom( description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/API-1d71197c19ed819f8f70fb437e9ce62b?p=2241197c19ed816993c3c5ae17d6f099&pm=s) 채팅방 목록을 조회합니다.\n" ) @GetMapping("/rooms") - public BaseResponse> getChatRoomList( + public BaseResponse> getChatRoomList( @AuthenticationPrincipal PrincipalDetails pd ) { Long memberId = pd.getMember().getId(); @@ -66,70 +60,21 @@ public BaseResponse> "- roomId: Request Body, Long\n" + "- senderId: Request Body, Long\n"+ "- receiverId: Request Body, Long\n" + - "- message: Request Body, String\n" + "- message: Request Body, String\n" + + "- unreadCountForSender: Request Body, int\n" ) @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { - - // 1. 서비스 호출 (모든 비즈니스 로직 위임) + // 1. 서비스 호출 MessageHandlingResult result = chatService.handleMessage(request); - // 2. [항상 전송] 채팅방 메시지 전송 - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), result.sendMessageResponseDTO()); - + redisPublisher.publishChatMessage(request.roomId(), result.sendMessageResponseDTO()); // 3. [조건부 전송] 채팅방 목록 업데이트 전송 if (result.hasRoomUpdates()) { - simpMessagingTemplate.convertAndSendToUser( - result.receiverId().toString(), - "/queue/updates", - result.chatRoomUpdateDTO() - ); + redisPublisher.publishChatRoomUpdate(result.receiverId(), result.chatRoomUpdateDTO()); } } -// @Transactional -// @MessageMapping("/send") -// public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { -// // 먼저 접속 여부 확인 후 unreadCount 계산 -// boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); -// int unreadForSender = receiverInRoom ? 0 : 1; -// request.setUnreadCountForSender(unreadForSender); -// -// ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); -// simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); -// -// if (!receiverInRoom) { -// Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( -// request.getRoomId(), -// request.getReceiverId() -// ); -// -// ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() -// .roomId(request.getRoomId()) -// .lastMessage(saved.message()) -// .lastMessageTime(saved.sentAt()) -// .unreadCount(totalUnreadCount) -// .build(); -// -// simpMessagingTemplate.convertAndSendToUser( -// request.getReceiverId().toString(), -// "/queue/updates", -// updateDTO -// ); -// Member sender = memberRepository.findById(request.getSenderId()).orElse(null); -// String senderName; -// if (sender.getRole()== UserRole.ADMIN) { -// senderName = sender.getAdminProfile().getName(); -// } else { -// senderName = sender.getPartnerProfile().getName(); -// } -// -// log.info(">>>>>>>>메시지 전송은 될걸"); -// notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); -// log.info(">>>>>>>>알림이 가나"); -// } -// } - @Operation( summary = "메시지 읽음 처리 API", description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81ffa771cb18ab157b54&pm=s) 메시지를 읽음처리합니다.\n"+ @@ -178,8 +123,8 @@ public BaseResponse leaveChattingR @Operation( summary = "상대방을 차단하는 API" + "상대방을 차단합니다. 메시지를 주고받을 수 없습니다.", - description = "# [v1.0 (2025-09-25)]() 상대방을 차단합니다.\n"+ - "- memberId: Request Body, Long\n" + description = "# [v1.0 (2025-09-25)](https://clumsy-seeder-416.notion.site/2db1197c19ed804ba3dbf57ba36860c4) 상대방을 차단합니다.\n"+ + "- opponentId: Request Body, Long\n" ) @PostMapping("/block") public BaseResponse block( @@ -187,14 +132,14 @@ public BaseResponse block( @RequestBody BlockRequestDTO.BlockMemberRequestDTO request ) { Long memberId = pd.getMember().getId(); - return BaseResponse.onSuccess(SuccessStatus._OK, blockService.blockMember(memberId, request.getOpponentId())); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.blockMember(memberId, request.opponentId())); } @Operation( summary = "상대방을 차단했는지 확인하는 API" + "상대방을 차단했는지 여부를 알려줍니다.", - description = "# [v1.0 (2025-09-25)]() 상대방을 차단했는지 검사합니다.\n"+ - "- memberId: Request Body, Long\n" + description = "# [v1.0 (2025-09-25)](https://clumsy-seeder-416.notion.site/2db1197c19ed80769521eab9660ac53f) 상대방을 차단했는지 검사합니다.\n"+ + "- opponentId: Request Body, Long\n" ) @GetMapping("/check/block/{opponentId}") public BaseResponse checkBlock( @@ -208,8 +153,8 @@ public BaseResponse checkBlock( @Operation( summary = "상대방을 차단 해제하는 API" + "상대방을 차단해제합니다. 앞으로 다시 메시지를 주고받을 수 있습니다.", - description = "# [v1.0 (2025-09-25)]() 상대방을 차단 해제합니다.\n"+ - "- memberId: Request Body, Long\n" + description = "# [v1.0 (2025-09-25)](https://clumsy-seeder-416.notion.site/2db1197c19ed80b6a93fcbe277fc934c?pvs=74) 상대방을 차단 해제합니다.\n"+ + "- opponentId: Request Body, Long\n" ) @DeleteMapping("/unblock") public BaseResponse unblock( @@ -223,8 +168,7 @@ public BaseResponse unblock( @Operation( summary = "차단한 대상을 조회합니다." + "본인이 차단한 대상을 모두 조회합니다.", - description = "# [v1.0 (2025-09-25)]() 차단한 대상을 조회합니다..\n"+ - "- memberId: Request Body, Long\n" + description = "# [v1.0 (2025-09-25)](https://clumsy-seeder-416.notion.site/2db1197c19ed8000b047d9857bcbbb2f) 차단한 대상을 조회합니다..\n" ) @GetMapping("/blockList") public BaseResponse> getBlockList( diff --git a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java deleted file mode 100644 index aea2beb..0000000 --- a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.assu.server.domain.chat.converter; - -import com.assu.server.domain.chat.dto.BlockResponseDTO; -import com.assu.server.domain.chat.entity.Block; -import com.assu.server.domain.common.enums.UserRole; -import com.assu.server.domain.member.entity.Member; - -import java.util.List; -import java.util.stream.Collectors; - -public class BlockConverter { - public static BlockResponseDTO.BlockMemberDTO toBlockDTO(Long blockedId, String blockedName) { - return BlockResponseDTO.BlockMemberDTO.builder() - .memberId(blockedId) - .name(blockedName) - .build(); - } - - public static BlockResponseDTO.CheckBlockMemberDTO toCheckBlockDTO(Long blockedId, String blockedName, boolean blocked) { - return BlockResponseDTO.CheckBlockMemberDTO.builder() - .memberId(blockedId) - .name(blockedName) - .blocked(blocked) - .build(); - } - - public static BlockResponseDTO.BlockMemberDTO toBlockedMemberDTO(Block block) { - // Block 엔티티에서 차단된 사용자(Member) 정보를 꺼냅니다. - Member blockedMember = block.getBlocked(); - UserRole blockedRole = blockedMember.getRole(); - String blockedName; - if (blockedRole == UserRole.ADMIN) { - blockedName = blockedMember.getAdminProfile().getName(); - } else { - blockedName = blockedMember.getPartnerProfile().getName(); - } - - return BlockResponseDTO.BlockMemberDTO.builder() - .memberId(blockedMember.getId()) - .name(blockedName) // 또는 getNickname() 등 실제 필드명 사용 - .blockDate(block.getCreatedAt()) - .build(); - } - - public static List toBlockedMemberListDTO(List blockList) { - return blockList.stream() - .map(BlockConverter::toBlockedMemberDTO) // 각 Block 객체에 대해 위 헬퍼 메소드를 호출 - .collect(Collectors.toList()); - } - -} diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java deleted file mode 100644 index 1d38eae..0000000 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.assu.server.domain.chat.converter; - -import com.assu.server.domain.admin.entity.Admin; -import com.assu.server.domain.chat.entity.enums.MessageType; -import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.chat.dto.ChatMessageDTO; -import com.assu.server.domain.chat.dto.ChatRequestDTO; -import com.assu.server.domain.chat.dto.ChatResponseDTO; -import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; -import com.assu.server.domain.chat.entity.ChattingRoom; -import com.assu.server.domain.chat.entity.Message; - -import com.assu.server.domain.partner.entity.Partner; - -import java.util.List; -import java.util.stream.Collectors; - -public class ChatConverter { - - // 채팅방 리스트 아이템 하나 - public static ChatRoomListResultDTO toChatRoomResultDTO(ChatRoomListResultDTO request) { - return ChatRoomListResultDTO.builder() - .roomId(request.getRoomId()) - .lastMessage(request.getLastMessage()) - .lastMessageTime(request.getLastMessageTime()) - .unreadMessagesCount(request.getUnreadMessagesCount()) - .opponentId(request.getOpponentId()) - .opponentName(request.getOpponentName()) - .opponentProfileImage(request.getOpponentProfileImage()) - .phoneNumber(request.getPhoneNumber()) - .build(); - } - - // 리스트 변환 - public static List toChatRoomListResultDTO(List dto) { - return dto.stream() - .map(ChatConverter::toChatRoomResultDTO) - .collect(Collectors.toList()); - } - - public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { - return ChattingRoom.builder() - .admin(admin) - .partner(partner) - .build(); - } - - public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) { - return ChatResponseDTO.CreateChatRoomResponseDTO.builder() - .roomId(room.getId()) - .adminViewName(room.getPartner().getName()) - .partnerViewName(room.getAdmin().getName()) - .isNew(true) - .build(); - } - - public static ChatResponseDTO.CreateChatRoomResponseDTO toEnterChatRoomDTO(ChattingRoom room) { - return ChatResponseDTO.CreateChatRoomResponseDTO.builder() - .roomId(room.getId()) - .adminViewName(room.getPartner().getName()) - .partnerViewName(room.getAdmin().getName()) - .isNew(false) - .build(); - } - - public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) { - return Message.builder() - .chattingRoom(room) - .sender(sender) - .receiver(receiver) - .message(request.getMessage()) - .unreadCount(request.getUnreadCountForSender()) - .type(MessageType.TEXT) - .build(); - } - - public static Message toGuideMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) { - return Message.builder() - .chattingRoom(room) - .sender(sender) - .receiver(receiver) - .message(request.getMessage()) - .unreadCount(0) - .type(MessageType.GUIDE) - .build(); - } - - public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message message) { - return ChatResponseDTO.SendMessageResponseDTO.builder() - .messageId(message.getId()) - .roomId(message.getChattingRoom().getId()) - .senderId(message.getSender().getId()) - .receiverId(message.getReceiver().getId()) - .message(message.getMessage()) - .sentAt(message.getCreatedAt()) - .messageType(message.getType()) - .unreadCountForSender(message.getUnreadCount()) - .build(); - } - -// public static ChatMessageDTO toChatMessageDTO(Message message, Long currentUserId) { -// return ChatMessageDTO.builder() -// .messageId(message.getId()) -// .message(message.getMessage()) -// .sendTime(message.getCreatedAt()) -// .isRead(message.isRead()) -// .isMyMessage(message.getSender().getId().equals(currentUserId)) -// .build(); -// } - - public static ChatResponseDTO.ChatHistoryResponseDTO toChatHistoryDTO( - Long roomId, - List messages) { - - // ③ 최종 DTO 빌드 - return ChatResponseDTO.ChatHistoryResponseDTO.builder() - .roomId(roomId) - .messages(messages) - .build(); - } -} diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java index 938b4f8..9ff823b 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java @@ -1,13 +1,8 @@ package com.assu.server.domain.chat.dto; -import lombok.Getter; -import lombok.Setter; - public class BlockRequestDTO { - @Getter - @Setter - public static class BlockMemberRequestDTO { - private Long opponentId; - } + public record BlockMemberRequestDTO( + Long opponentId + ) {} } diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java index e13d1db..961ffc8 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java @@ -1,32 +1,77 @@ package com.assu.server.domain.chat.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; public class BlockResponseDTO { - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class BlockMemberDTO { - private Long memberId; - private String name; - private LocalDateTime blockDate; + public record BlockMemberDTO ( + Long memberId, + String name, + LocalDateTime blockDate + ) { + public static BlockMemberDTO toBlockDTO( + Long blockedId, + String blockedName + ) { + return new BlockMemberDTO( + blockedId, + blockedName, + LocalDateTime.now() + ); + } + + public static BlockMemberDTO toBlockedMemberDTO( + Block block + ) { + Member blockedMember = block.getBlocked(); + UserRole blockedRole = blockedMember.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blockedMember.getAdminProfile().getName(); + } else { + blockedName = blockedMember.getPartnerProfile().getName(); + } + + return new BlockMemberDTO( + blockedMember.getId(), + blockedName, + block.getCreatedAt() + ); + } + + public static List toBlockedMemberListDTO( + List blockList + ) { + return blockList.stream() + .map(BlockResponseDTO.BlockMemberDTO::toBlockedMemberDTO) + .collect(Collectors.toList()); + } + } - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class CheckBlockMemberDTO { - private Long memberId; - private String name; - private boolean blocked; + + public record CheckBlockMemberDTO ( + Long memberId, + String name, + boolean blocked + ) { + public static CheckBlockMemberDTO toCheckBlockDTO( + Long blockedId, + String blockedName, + boolean blocked + ) { + return new CheckBlockMemberDTO( + blockedId, + blockedName, + blocked + ); + } } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java index 9886cba..93676fd 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java @@ -3,34 +3,27 @@ import com.assu.server.domain.chat.entity.enums.MessageType; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; import java.time.LocalDateTime; -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ChatMessageDTO { - @JsonIgnore - private Long roomId; - // 메시지 삭제 시 사용 가능 - private Long messageId; +public record ChatMessageDTO( + @JsonIgnore + Long roomId, + // 메시지 삭제 시 사용 가능 + Long messageId, - private String message; - private LocalDateTime sendTime; + String message, + LocalDateTime sendTime, - @JsonProperty("unreadCountForSender") - private Integer unreadCount; + @JsonProperty("unreadCountForSender") + Integer unreadCount, - @JsonProperty("isRead") - private boolean isRead; + @JsonProperty("isRead") + boolean isRead, - @JsonProperty("isMyMessage") - private boolean isMyMessage; + @JsonProperty("isMyMessage") + boolean isMyMessage, - private MessageType messageType; + MessageType messageType +){ } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index c518055..5efc956 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,27 +1,17 @@ package com.assu.server.domain.chat.dto; -import com.assu.server.domain.chat.entity.enums.MessageType; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - public class ChatRequestDTO { - @Getter - public static class CreateChatRoomRequestDTO { - private Long adminId; - private Long partnerId; - } - @Getter - @Setter - @AllArgsConstructor - @NoArgsConstructor - public static class ChatMessageRequestDTO { - private Long roomId; - private Long senderId; - private Long receiverId; - private String message; - private int unreadCountForSender; - } -} \ No newline at end of file + public record CreateChatRoomRequestDTO( + Long adminId, + Long partnerId + ) {} + + public record ChatMessageRequestDTO( + Long roomId, + Long senderId, + Long receiverId, + String message + ) {} + +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 9a8f3d9..d66f6b6 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -1,31 +1,41 @@ package com.assu.server.domain.chat.dto; +import com.assu.server.domain.chat.entity.ChattingRoom; +import com.assu.server.domain.chat.entity.Message; import com.assu.server.domain.chat.entity.enums.MessageType; import com.fasterxml.jackson.annotation.JsonFormat; -import com.google.protobuf.Enum; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; + import java.time.LocalDateTime; import java.util.List; public class ChatResponseDTO { // 채팅방 목록 조회 - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class CreateChatRoomResponseDTO { - private Long roomId; - private String adminViewName; - private String partnerViewName; - private Boolean isNew; + public record CreateChatRoomResponseDTO( + Long roomId, + String adminViewName, + String partnerViewName, + Boolean isNew + ) { + public static CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) { + return new CreateChatRoomResponseDTO( + room.getId(), + room.getPartner().getName(), + room.getAdmin().getName(), + true + ); + } + public static CreateChatRoomResponseDTO toEnterChatRoomDTO(ChattingRoom room) { + return new CreateChatRoomResponseDTO( + room.getId(), + room.getPartner().getName(), + room.getAdmin().getName(), + false + ); + } } // 메시지 전송 - @Builder public record SendMessageResponseDTO( Long messageId, Long roomId, @@ -42,6 +52,23 @@ public SendMessageResponseDTO withUnreadCountForSender(Integer count) { messageId, roomId, senderId, receiverId, message, messageType, sentAt, count ); } + + public static SendMessageResponseDTO toSendMessageDTO( + Message message + ) { + return new SendMessageResponseDTO( + message.getId(), + message.getChattingRoom().getId(), + message.getSender().getId(), + message.getReceiver().getId(), + message.getMessage(), + message.getType(), + message.getCreatedAt(), + message.getUnreadCount() + ); + + + } } // 메시지 읽음 처리 @@ -54,23 +81,26 @@ public record ReadMessageResponseDTO( ) {} // 채팅방 들어갔을 때 조회 - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class ChatHistoryResponseDTO { - private Long roomId; - private List messages; + public record ChatHistoryResponseDTO( + Long roomId, + List messages + ) { + public static ChatHistoryResponseDTO toChatHistoryDTO( + Long roomId, + List messages) { + return new ChatHistoryResponseDTO( + roomId, + messages + ); + } } - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class LeaveChattingRoomResponseDTO { - private Long roomId; - private boolean isLeftSuccessfully; - private boolean isRoomDeleted; + public record LeaveChattingRoomResponseDTO( + Long roomId, + boolean isLeftSuccessfully, + boolean isRoomDeleted + ) { + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java index 4e57cb8..d161b97 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java @@ -1,23 +1,42 @@ package com.assu.server.domain.chat.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public record ChatRoomListResultDTO( + Long roomId, + String lastMessage, + LocalDateTime lastMessageTime, + Long unreadMessagesCount, + Long opponentId, + String opponentName, + String opponentProfileImage, + String phoneNumber + +) { + // 채팅방 리스트 아이템 하나 + public static ChatRoomListResultDTO toChatRoomResultDTO( + ChatRoomListResultDTO dto + ) { + return new ChatRoomListResultDTO( + dto.roomId, + dto.lastMessage, + dto.lastMessageTime, + dto.unreadMessagesCount, + dto.opponentId, + dto.opponentName, + dto.opponentProfileImage, + dto.phoneNumber + ); + } -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ChatRoomListResultDTO { - private Long roomId; - private String lastMessage; - private LocalDateTime lastMessageTime; - private Long unreadMessagesCount; - private Long opponentId; - private String opponentName; - private String opponentProfileImage; - private String phoneNumber; + // 채팅방 리스트 변환 + public static List toChatRoomListResultDTO ( + List dto + ) { + return dto.stream() + .map(ChatRoomListResultDTO::toChatRoomResultDTO) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java index b83ee0c..ce872d8 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java @@ -1,15 +1,24 @@ package com.assu.server.domain.chat.dto; -import lombok.Builder; -import lombok.Getter; - import java.time.LocalDateTime; -@Getter -@Builder -public class ChatRoomUpdateDTO { - private Long roomId; - private String lastMessage; - private LocalDateTime lastMessageTime; - private Long unreadCount; // 해당 채팅방의 총 안읽은 메시지 수 +public record ChatRoomUpdateDTO( + Long roomId, + String lastMessage, + LocalDateTime lastMessageTime, + Long unreadCount // 해당 채팅방의 총 안읽은 메시지 수 +) { + public static ChatRoomUpdateDTO toChatRoomUpdateDTO( + Long roomId, + String lastMessage, + LocalDateTime lastMessageTime, + Long unreadCount + ) { + return new ChatRoomUpdateDTO( + roomId, + lastMessage, + lastMessageTime, + unreadCount + ); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java b/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java index 56c142e..43f73a6 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java +++ b/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java @@ -5,22 +5,15 @@ public record MessageHandlingResult( ChatRoomUpdateDTO chatRoomUpdateDTO, Long receiverId ) { - - // 정적 팩토리 메소드 1 public static MessageHandlingResult of(ChatResponseDTO.SendMessageResponseDTO sendMessageDTO) { - // record의 기본 생성자를 호출합니다. return new MessageHandlingResult(sendMessageDTO, null, null); } - // 정적 팩토리 메소드 2 public static MessageHandlingResult withUpdates(ChatResponseDTO.SendMessageResponseDTO sendMessageDTO, ChatRoomUpdateDTO updateDTO, Long receiverId) { - // record의 기본 생성자를 호출합니다. return new MessageHandlingResult(sendMessageDTO, updateDTO, receiverId); } - // 헬퍼(Helper) 메소드 public boolean hasRoomUpdates() { - // record는 'get' 접두사 없는 접근자(chatRoomUpdateDTO())를 사용합니다. return chatRoomUpdateDTO != null; } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java index e88c405..a293183 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -4,7 +4,6 @@ import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.partner.entity.Partner; - import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -64,4 +63,11 @@ public void setAdmin(Admin admin) { public void setPartner(Partner partner) { this.partner = partner; } + + public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { + return ChattingRoom.builder() + .admin(admin) + .partner(partner) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index ba06e81..5faa607 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -1,12 +1,14 @@ package com.assu.server.domain.chat.entity; -import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.entity.enums.MessageType; - import com.assu.server.domain.common.entity.BaseEntity; - +import com.assu.server.domain.member.entity.Member; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @@ -51,4 +53,37 @@ public void markAsRead() { this.isRead = true; this.unreadCount = 0; } + + public static Message toMessageEntity( + ChatRequestDTO.ChatMessageRequestDTO request, + ChattingRoom room, + Member sender, + Member receiver, + int unreadForSender + ) { + return Message.builder() + .chattingRoom(room) + .sender(sender) + .receiver(receiver) + .message(request.message()) + .unreadCount(unreadForSender) + .type(MessageType.TEXT) + .build(); + } + + public static Message toGuideMessageEntity( + ChatRequestDTO.ChatMessageRequestDTO request, + ChattingRoom room, + Member sender, + Member receiver + ) { + return Message.builder() + .chattingRoom(room) + .sender(sender) + .receiver(receiver) + .message(request.message()) + .unreadCount(0) + .type(MessageType.GUIDE) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/redis/RedisMessageSubscriber.java b/src/main/java/com/assu/server/domain/chat/redis/RedisMessageSubscriber.java new file mode 100644 index 0000000..fa79b38 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/redis/RedisMessageSubscriber.java @@ -0,0 +1,57 @@ +package com.assu.server.domain.chat.redis; + +import com.assu.server.domain.chat.dto.ChatResponseDTO; +import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisMessageSubscriber implements MessageListener { + + private final SimpMessagingTemplate messagingTemplate; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public void onMessage(Message message, byte[] pattern) { + try { + // RedisTemplate의 Serializer를 통해 byte[]를 String으로 변환 + String publishMessage = redisTemplate.getStringSerializer().deserialize(message.getBody()); + String channel = redisTemplate.getStringSerializer().deserialize(message.getChannel()); + + if (channel == null || publishMessage == null) return; + + if (channel.startsWith("chat.room.")) { + // 채팅방 메시지 + handleChatMessage(channel, publishMessage); + } else if (channel.startsWith("user.update.")) { + // 채팅 목록 업데이트 + handleUserUpdate(channel, publishMessage); + } + } catch (Exception e) { + log.error("Redis 메시지 처리 실패 {}", e.getMessage()); + } + } + + private void handleChatMessage(String channel, String jsonMessage) throws Exception { + String roomId = channel.substring("chat.room.".length()); + ChatResponseDTO.SendMessageResponseDTO messageDto = + objectMapper.readValue(jsonMessage, ChatResponseDTO.SendMessageResponseDTO.class); + messagingTemplate.convertAndSend("/sub/chat/" + roomId, messageDto); + } + + public void handleUserUpdate(String channel, String jsonMessage) throws Exception { + String userId = channel.substring("user.update.".length()); + ChatRoomUpdateDTO updateDTO = + objectMapper.readValue(jsonMessage, ChatRoomUpdateDTO.class); + messagingTemplate.convertAndSend("/sub/chat/list/" + userId, updateDTO); + } +} diff --git a/src/main/java/com/assu/server/domain/chat/redis/RedisPublisher.java b/src/main/java/com/assu/server/domain/chat/redis/RedisPublisher.java new file mode 100644 index 0000000..5c5931f --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/redis/RedisPublisher.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.chat.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RedisPublisher { + + private final RedisTemplate redisTemplate; + + // 채팅방 메시지 발행 + public void publishChatMessage(Long roomId, Object message) { + redisTemplate.convertAndSend("chat.room." + roomId, message); + } + + // 채팅 목록 업데이트 발행 + public void publishChatRoomUpdate(Long userId, Object updateDto) { + redisTemplate.convertAndSend("user.update." + userId, updateDto); + } +} diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java index b9edc56..86b25a3 100644 --- a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java @@ -1,7 +1,5 @@ package com.assu.server.domain.chat.service; -import com.assu.server.domain.chat.converter.BlockConverter; -import com.assu.server.domain.chat.converter.ChatConverter; import com.assu.server.domain.chat.dto.BlockResponseDTO; import com.assu.server.domain.chat.entity.Block; import com.assu.server.domain.chat.repository.BlockRepository; @@ -15,7 +13,6 @@ import org.springframework.stereotype.Service; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -59,7 +56,7 @@ public BlockResponseDTO.BlockMemberDTO blockMember(Long blockerId, Long blockedI blockRepository.save(block); - return BlockConverter.toBlockDTO(blockedId, blockedName); + return BlockResponseDTO.BlockMemberDTO.toBlockDTO(blockedId, blockedName); } @Override @@ -81,10 +78,10 @@ public BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long bloc } if (blockRepository.existsBlockRelationBetween(blocker, blocked)) { - return BlockConverter.toCheckBlockDTO(blockedId, blockedName, true); + return BlockResponseDTO.CheckBlockMemberDTO.toCheckBlockDTO(blockedId, blockedName, true); } else { - return BlockConverter.toCheckBlockDTO(blockedId, blockedName, false); + return BlockResponseDTO.CheckBlockMemberDTO.toCheckBlockDTO(blockedId, blockedName, false); } } @@ -108,7 +105,7 @@ public BlockResponseDTO.BlockMemberDTO unblockMember(Long blockerId, Long blocke // Transactional 환경에서는 Dirty-checking으로 delete 쿼리가 나갑니다. blockRepository.deleteByBlockerAndBlocked(blocker, blocked); - return BlockConverter.toBlockDTO(blockedId, blockedName); + return BlockResponseDTO.BlockMemberDTO.toBlockDTO(blockedId, blockedName); } @Transactional @@ -119,6 +116,6 @@ public List getMyBlockList(Long blockerId) { List blockList = blockRepository.findByBlocker(blocker); - return BlockConverter.toBlockedMemberListDTO(blockList); + return BlockResponseDTO.BlockMemberDTO.toBlockedMemberListDTO(blockList); } } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index 10c84fb..a2b3fd8 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -10,7 +10,6 @@ public interface ChatService { List getChatRoomList(Long memberId); ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId); -// ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId); ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId); diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index c3e2558..e584476 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -2,15 +2,14 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; -import com.assu.server.domain.chat.converter.ChatConverter; import com.assu.server.domain.chat.dto.*; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.entity.Message; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.common.enums.UserRole; import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.domain.partner.entity.Partner; @@ -48,14 +47,14 @@ public class ChatServiceImpl implements ChatService { public List getChatRoomList(Long memberId) { List chatRoomList = chatRepository.findChattingRoomsByMemberId(memberId); - return ChatConverter.toChatRoomListResultDTO(chatRoomList); + return ChatRoomListResultDTO.toChatRoomListResultDTO(chatRoomList); } @Override public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId) { - Long adminId = request.getAdminId(); - Long partnerId = request.getPartnerId(); + Long adminId = request.adminId(); + Long partnerId = request.partnerId(); Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); @@ -72,7 +71,7 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C boolean isExist = chatRepository.checkChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); if(!isExist) { - ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); + ChattingRoom room = ChattingRoom.toCreateChattingRoom(admin, partner); room.updateStatus(ActivationStatus.ACTIVE); @@ -83,10 +82,10 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C admin.getName() ); ChattingRoom savedRoom = chatRepository.save(room); - return ChatConverter.toCreateChatRoomIdDTO(savedRoom); + return ChatResponseDTO.CreateChatRoomResponseDTO.toCreateChatRoomIdDTO(savedRoom); } else { ChattingRoom existChatRoom = chatRepository.findChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); - return ChatConverter.toEnterChatRoomDTO(existChatRoom); + return ChatResponseDTO.CreateChatRoomResponseDTO.toEnterChatRoomDTO(existChatRoom); } } @@ -115,41 +114,39 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C @Transactional public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 1. 유효성 검사 (기존 로직) - ChattingRoom room = chatRepository.findById(request.getRoomId()) + ChattingRoom room = chatRepository.findById(request.roomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); - Member sender = memberRepository.findById(request.getSenderId()) + Member sender = memberRepository.findById(request.senderId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); - Member receiver = memberRepository.findById(request.getReceiverId()) + Member receiver = memberRepository.findById(request.receiverId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); // 2. 컨트롤러에서 가져온 비즈니스 로직 (접속 확인) - boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); + boolean receiverInRoom = presenceTracker.isInRoom(request.receiverId(), request.roomId()); int unreadForSender = receiverInRoom ? 0 : 1; - request.setUnreadCountForSender(unreadForSender); // 3. 메시지 저장 (기존 로직) - Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); + Message message = Message.toMessageEntity(request, room, sender, receiver, unreadForSender); Message saved = messageRepository.saveAndFlush(message); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); - ChatResponseDTO.SendMessageResponseDTO savedDTO = ChatConverter.toSendMessageDTO(saved); + ChatResponseDTO.SendMessageResponseDTO savedDTO = ChatResponseDTO.SendMessageResponseDTO.toSendMessageDTO(saved); // 4. 컨트롤러에서 가져온 비즈니스 로직 (수신자 부재 시) if (!receiverInRoom) { // 4-1. 안 읽은 수 계산 Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( - request.getRoomId(), - request.getReceiverId() + request.roomId(), + request.receiverId() ); // 4-2. 채팅방 목록 업데이트 DTO 생성 - ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() - .roomId(request.getRoomId()) - .lastMessage(savedDTO.message()) - .lastMessageTime(savedDTO.sentAt()) - .unreadCount(totalUnreadCount) - .build(); + ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.toChatRoomUpdateDTO( + request.roomId(), + savedDTO.message(), + savedDTO.sentAt(), + totalUnreadCount); // 4-3. 발신자 이름 찾기 (기존 컨트롤러 로직) String senderName; @@ -160,10 +157,10 @@ public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO } // 4-4. 알림 전송 - notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); + notificationCommandService.sendChat(request.receiverId(), request.roomId(), senderName, request.message()); // 5. [업데이트 포함] 결과 반환 - return MessageHandlingResult.withUpdates(savedDTO, updateDTO, request.getReceiverId()); + return MessageHandlingResult.withUpdates(savedDTO, updateDTO, request.receiverId()); } // 5. [일반 메시지] 결과 반환 @@ -175,18 +172,18 @@ public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO @Transactional public ChatResponseDTO.SendMessageResponseDTO sendGuideMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 유효성 검사 - ChattingRoom room = chatRepository.findById(request.getRoomId()) + ChattingRoom room = chatRepository.findById(request.roomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); - Member sender = memberRepository.findById(request.getSenderId()) + Member sender = memberRepository.findById(request.senderId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); - Member receiver = memberRepository.findById(request.getReceiverId()) + Member receiver = memberRepository.findById(request.receiverId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); - Message message = ChatConverter.toGuideMessageEntity(request, room, sender, receiver); + Message message = Message.toGuideMessageEntity(request, room, sender, receiver); Message saved = messageRepository.saveAndFlush(message); - ChatResponseDTO.SendMessageResponseDTO responseDTO = ChatConverter.toSendMessageDTO(saved); - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), responseDTO); + ChatResponseDTO.SendMessageResponseDTO responseDTO = ChatResponseDTO.SendMessageResponseDTO.toSendMessageDTO(saved); + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), responseDTO); return responseDTO; } @@ -215,7 +212,7 @@ public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memb List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(room.getId(), memberId); - return ChatConverter.toChatHistoryDTO(room.getId(), allMessages); + return ChatResponseDTO.ChatHistoryResponseDTO.toChatHistoryDTO(room.getId(), allMessages); } @Override diff --git a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java index 76679a2..8fabad4 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java @@ -266,8 +266,7 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par chattingRoom.getId(), partnerId, adminId, - guideMessage, - 0 + guideMessage ); chatService.sendGuideMessage(guideMessageRequest); notificationService.sendChat(adminId, chattingRoom.getId(), partner.getName(), guideMessage); @@ -278,8 +277,7 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par chattingRoom.getId(), adminId, partnerId, - guideMessage, - 0 + guideMessage ); chatService.sendGuideMessage(guideMessageRequest); notificationService.sendChat(partnerId, chattingRoom.getId(), admin.getName(), guideMessage); @@ -407,8 +405,7 @@ public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(Part chattingRoom.getId(), admin.getId(), partner.getId(), - guideMessage, - 0 + guideMessage ); // 5. 완성된 DTO를 사용해서 안내 메시지를 전송합니다. diff --git a/src/main/java/com/assu/server/global/config/RedisConfig.java b/src/main/java/com/assu/server/global/config/RedisConfig.java index 135c0c8..49c54e3 100644 --- a/src/main/java/com/assu/server/global/config/RedisConfig.java +++ b/src/main/java/com/assu/server/global/config/RedisConfig.java @@ -1,5 +1,6 @@ package com.assu.server.global.config; +import com.assu.server.domain.chat.redis.RedisMessageSubscriber; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -9,11 +10,15 @@ import org.springframework.context.annotation.Profile; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @RequiredArgsConstructor +@EnableRedisRepositories @Profile("!test") public class RedisConfig { @@ -38,4 +43,18 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC return redisTemplate; } + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory redisConnectionFactory, + RedisMessageSubscriber subscriber + ) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(redisConnectionFactory); + + container.addMessageListener(subscriber, new PatternTopic("chat.room.*")); + container.addMessageListener(subscriber, new PatternTopic("user.update.*")); + + return container; + } + } \ No newline at end of file diff --git a/src/main/java/com/assu/server/global/config/WebSocketConfig.java b/src/main/java/com/assu/server/global/config/WebSocketConfig.java new file mode 100644 index 0000000..7e669cc --- /dev/null +++ b/src/main/java/com/assu/server/global/config/WebSocketConfig.java @@ -0,0 +1,54 @@ +package com.assu.server.global.config; + +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; + +import com.assu.server.domain.certification.config.StompAuthChannelInterceptor; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSocketMessageBroker +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompAuthChannelInterceptor stompAuthChannelInterceptor; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + + registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 지점 + .setAllowedOriginPatterns( + "*", + "https://assu.shop", + "http://localhost:63342", + "http://localhost:5173", // Vite 기본 + "http://localhost:3000", // CRA/Next 기본 + "http://127.0.0.1:*", + "http://192.168.*.*:*"); + // 채팅용 엔드포인트 + + // 인증용 엔드포인트 + registry.addEndpoint("/ws-certify") + .setAllowedOriginPatterns("*"); + + + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 채팅용 & 인증용 합치기 + registry.enableSimpleBroker("/sub","/queue", "/certification"); + registry.setApplicationDestinationPrefixes("/pub", "/app"); // 둘 다 추가 + registry.setUserDestinationPrefix("/user"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompAuthChannelInterceptor); + } +} \ No newline at end of file diff --git a/src/main/resources/redisChattingTest.html b/src/main/resources/redisChattingTest.html new file mode 100644 index 0000000..45862ff --- /dev/null +++ b/src/main/resources/redisChattingTest.html @@ -0,0 +1,150 @@ + + + + + Redis Pub/Sub 채팅 테스트 + + + + + + +
+
+

🚀 연결 설정

+ + + + + +
+ +

💬 메시지 전송

+ + + + + + + + + + + + + +
+ +
+

📜 실시간 로그

+
+
+
+ + + + \ No newline at end of file