diff --git a/build.gradle b/build.gradle index 210ac79..64d943c 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,18 @@ dependencies { //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + + // WebSocket + implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // MongoDB + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + + // JSON 파싱 + implementation 'com.fasterxml.jackson.core:jackson-databind' } tasks.named('test') { diff --git a/src/main/java/study/spring_boot_c/domain/chat/api/ChatController.java b/src/main/java/study/spring_boot_c/domain/chat/api/ChatController.java new file mode 100644 index 0000000..436ca34 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/api/ChatController.java @@ -0,0 +1,62 @@ +package study.spring_boot_c.domain.chat.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import study.spring_boot_c.domain.chat.application.ChatService; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; +import study.spring_boot_c.global.common.response.BaseResponse; +import study.spring_boot_c.global.error.code.status.SuccessStatus; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chat") +@Validated +public class ChatController { + + private final ChatService chatService; + + @PostMapping("/send") + @Operation(summary = "채팅 메시지 전송 API", description = "클라이언트가 채팅 메시지를 전송할 때 사용하는 API입니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CHAT_200", description = "메시지 전송 성공") + }) + @Parameters({ + @Parameter(name = "roomId", description = "채팅방 ID"), + @Parameter(name = "senderId", description = "메시지를 보낸 사용자 ID"), + @Parameter(name = "message", description = "보낼 메시지 내용") + }) + public BaseResponse sendChatMessage(@Valid @RequestBody ChatMessageDTO.MessageReceive request) { + + chatService.sendMessage(request); + + return BaseResponse.onSuccess(SuccessStatus.CHAT_SEND_SUCCESS, null); + } + + @GetMapping("/room/{roomId}/message") + @Operation(summary = "채팅방 메시지 조회 API", description = "클라이언트가 채팅 메시지를 전송할 때 사용하는 API입니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CHAT_200", description = "메시지 조회 성공") + }) + public BaseResponse> getMessagesByRoomId(@Valid @PathVariable Long roomId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + + Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").ascending()); + Page result = chatService.getMessagesByRoomId(roomId, pageable); + + return BaseResponse.onSuccess(SuccessStatus.CHAT_SEND_SUCCESS, result); + } +} + diff --git a/src/main/java/study/spring_boot_c/domain/chat/api/ChatWebSocketController.java b/src/main/java/study/spring_boot_c/domain/chat/api/ChatWebSocketController.java new file mode 100644 index 0000000..299b07c --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/api/ChatWebSocketController.java @@ -0,0 +1,20 @@ +package study.spring_boot_c.domain.chat.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; +import study.spring_boot_c.domain.chat.application.ChatService; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; + +@Controller +@RequiredArgsConstructor +public class ChatWebSocketController { + + private final ChatService chatService; + + @MessageMapping("/chat/send") + public void handleWebSocketMessage(ChatMessageDTO.MessageReceive message) { + chatService.sendMessage(message); + } +} + diff --git a/src/main/java/study/spring_boot_c/domain/chat/application/ChatService.java b/src/main/java/study/spring_boot_c/domain/chat/application/ChatService.java new file mode 100644 index 0000000..2876420 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/application/ChatService.java @@ -0,0 +1,11 @@ +package study.spring_boot_c.domain.chat.application; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; + +public interface ChatService { + void sendMessage(ChatMessageDTO.MessageReceive dto); + + Page getMessagesByRoomId(Long roomId, Pageable pageable); +} diff --git a/src/main/java/study/spring_boot_c/domain/chat/application/ChatServiceImpl.java b/src/main/java/study/spring_boot_c/domain/chat/application/ChatServiceImpl.java new file mode 100644 index 0000000..088c9c6 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/application/ChatServiceImpl.java @@ -0,0 +1,52 @@ +package study.spring_boot_c.domain.chat.application; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import study.spring_boot_c.domain.chat.converter.ChatMessageConverter; +import study.spring_boot_c.domain.chat.domain.entity.ChatMessage; +import study.spring_boot_c.domain.chat.domain.repository.ChatMessageRepository; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; +import study.spring_boot_c.domain.chat.exception.ChatException; +import study.spring_boot_c.global.error.code.status.ErrorStatus; +import study.spring_boot_c.global.redis.RedisPublisher; + +@Service +@RequiredArgsConstructor +public class ChatServiceImpl implements ChatService{ + private final RedisPublisher redisPublisher; + private final ChatMessageRepository chatMessageRepository; // MongoDB 저장용 + private final ObjectMapper objectMapper; + + @Transactional + @Override + public void sendMessage(ChatMessageDTO.MessageReceive dto) { + // 멤버 체크 + 방 체크 구현 로직 필요 + ChatMessage message = ChatMessageConverter.toChatMessage(dto); + String channel = "chatroom:" + dto.getRoomId(); + + try { + chatMessageRepository.save(message); + } catch (Exception e) { + throw new ChatException(ErrorStatus.DB_ERROR); + } + + try { + String json = objectMapper.writeValueAsString(message); + redisPublisher.publish(channel, json); + } catch (Exception e) { + throw new ChatException(ErrorStatus.REDIS_ERROR); + } + + } + + @Override + public Page getMessagesByRoomId(Long roomId, Pageable pageable) { + return chatMessageRepository.findByRoomIdOrderByTimestampAsc(roomId, pageable) + .map(ChatMessageConverter::toRoomMessages); + } +} diff --git a/src/main/java/study/spring_boot_c/domain/chat/converter/ChatMessageConverter.java b/src/main/java/study/spring_boot_c/domain/chat/converter/ChatMessageConverter.java new file mode 100644 index 0000000..744592d --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/converter/ChatMessageConverter.java @@ -0,0 +1,28 @@ +package study.spring_boot_c.domain.chat.converter; + +import org.springframework.stereotype.Component; +import study.spring_boot_c.domain.chat.domain.entity.ChatMessage; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; + +import java.time.LocalDateTime; + +@Component +public class ChatMessageConverter { + + public static ChatMessage toChatMessage(ChatMessageDTO.MessageReceive dto) { + return ChatMessage.builder() + .roomId(dto.getRoomId()) + .senderId(dto.getSenderId()) + .message(dto.getMessage()) + .timestamp(LocalDateTime.now()) + .build(); + } + + public static ChatMessageDTO.RoomMessage toRoomMessages(ChatMessage entity) { + return ChatMessageDTO.RoomMessage.builder() + .senderId(entity.getSenderId()) + .message(entity.getMessage()) + .timestamp(entity.getTimestamp()) + .build(); + } +} diff --git a/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatMessage.java b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatMessage.java new file mode 100644 index 0000000..4620215 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatMessage.java @@ -0,0 +1,22 @@ +package study.spring_boot_c.domain.chat.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import study.spring_boot_c.domain.model.entity.BaseEntity; + +import java.time.LocalDateTime; + +@Builder +@Getter +@Document(collection = "chat_messages") +public class ChatMessage { + @Id + private String id; + private Long roomId; + private Long senderId; + private String message; + private LocalDateTime timestamp; +} + diff --git a/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatNotification.java b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatNotification.java new file mode 100644 index 0000000..a61f1d7 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatNotification.java @@ -0,0 +1,28 @@ +package study.spring_boot_c.domain.chat.domain.entity; + +import lombok.*; +import jakarta.persistence.*; +import study.spring_boot_c.domain.model.entity.BaseEntity; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ChatNotification extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long memberId; + private String previewMessage; + private boolean isRead; + private LocalDateTime notifiedAt; + + @ManyToOne + @JoinColumn(name = "chat_room_id") + private ChatRoom chatRoom; +} + diff --git a/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatRoom.java b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatRoom.java new file mode 100644 index 0000000..571f7e5 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/domain/entity/ChatRoom.java @@ -0,0 +1,26 @@ +package study.spring_boot_c.domain.chat.domain.entity; + +import jakarta.persistence.*; +import lombok.*; +import study.spring_boot_c.domain.model.entity.BaseEntity; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ChatRoom extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private LocalDateTime createdAt; + + @OneToMany(mappedBy = "chatRoom") + private List notifications; +} + diff --git a/src/main/java/study/spring_boot_c/domain/chat/domain/repository/ChatMessageRepository.java b/src/main/java/study/spring_boot_c/domain/chat/domain/repository/ChatMessageRepository.java new file mode 100644 index 0000000..d9db20a --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/domain/repository/ChatMessageRepository.java @@ -0,0 +1,10 @@ +package study.spring_boot_c.domain.chat.domain.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import study.spring_boot_c.domain.chat.domain.entity.ChatMessage; + +public interface ChatMessageRepository extends MongoRepository { + Page findByRoomIdOrderByTimestampAsc(Long roomId, Pageable pageable); +} diff --git a/src/main/java/study/spring_boot_c/domain/chat/dto/ChatMessageDTO.java b/src/main/java/study/spring_boot_c/domain/chat/dto/ChatMessageDTO.java new file mode 100644 index 0000000..24aa402 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/dto/ChatMessageDTO.java @@ -0,0 +1,32 @@ +package study.spring_boot_c.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +public class ChatMessageDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MessageReceive { + private Long roomId; + private Long senderId; + private String message; + private LocalDateTime timestamp; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class RoomMessage { + private Long senderId; + private String message; + private LocalDateTime timestamp; + } +} diff --git a/src/main/java/study/spring_boot_c/domain/chat/exception/ChatException.java b/src/main/java/study/spring_boot_c/domain/chat/exception/ChatException.java new file mode 100644 index 0000000..3137ae5 --- /dev/null +++ b/src/main/java/study/spring_boot_c/domain/chat/exception/ChatException.java @@ -0,0 +1,11 @@ +package study.spring_boot_c.domain.chat.exception; + +import study.spring_boot_c.global.error.code.BaseErrorCode; +import study.spring_boot_c.global.error.exception.GeneralException; + +public class ChatException extends GeneralException { + + public ChatException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/study/spring_boot_c/domain/member/domain/entity/Manner.java b/src/main/java/study/spring_boot_c/domain/member/domain/entity/Manner.java index 78b71ca..652241f 100644 --- a/src/main/java/study/spring_boot_c/domain/member/domain/entity/Manner.java +++ b/src/main/java/study/spring_boot_c/domain/member/domain/entity/Manner.java @@ -19,11 +19,11 @@ public class Manner extends BaseEntity { private int score; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "evaluatee_id", nullable = false) private Member evaluatee; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "evaluator_id", nullable = false) private Member evaluator; } diff --git a/src/main/java/study/spring_boot_c/domain/member/domain/entity/SaleReview.java b/src/main/java/study/spring_boot_c/domain/member/domain/entity/SaleReview.java index 2b8bfba..8178487 100644 --- a/src/main/java/study/spring_boot_c/domain/member/domain/entity/SaleReview.java +++ b/src/main/java/study/spring_boot_c/domain/member/domain/entity/SaleReview.java @@ -22,11 +22,11 @@ public class SaleReview extends BaseEntity { private String content; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "reviewer_id", nullable = false) private Member reviewer; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "reviewee_id", nullable = false) private Member reviewee; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/study/spring_boot_c/domain/notification/domain/entity/CarrotNotification.java b/src/main/java/study/spring_boot_c/domain/notification/domain/entity/CarrotNotification.java index c152b62..107bc2e 100644 --- a/src/main/java/study/spring_boot_c/domain/notification/domain/entity/CarrotNotification.java +++ b/src/main/java/study/spring_boot_c/domain/notification/domain/entity/CarrotNotification.java @@ -19,11 +19,11 @@ public class CarrotNotification extends BaseEntity { private int score; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "evaluatee_id", nullable = false) private Member evaluatee; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "evaluator_id", nullable = false) private Member evaluator; } diff --git a/src/main/java/study/spring_boot_c/domain/notification/domain/repository/CarrotNotificationRepository.java b/src/main/java/study/spring_boot_c/domain/notification/domain/repository/CarrotNotificationRepository.java index e7253b6..b343496 100644 --- a/src/main/java/study/spring_boot_c/domain/notification/domain/repository/CarrotNotificationRepository.java +++ b/src/main/java/study/spring_boot_c/domain/notification/domain/repository/CarrotNotificationRepository.java @@ -1,6 +1,7 @@ package study.spring_boot_c.domain.notification.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.spring_boot_c.domain.notification.domain.entity.CarrotNotification; -public interface CarrotNotificationRepository extends JpaRepository { +public interface CarrotNotificationRepository extends JpaRepository { } diff --git a/src/main/java/study/spring_boot_c/domain/product/domain/entity/Product.java b/src/main/java/study/spring_boot_c/domain/product/domain/entity/Product.java index 8816a6f..2de5c11 100644 --- a/src/main/java/study/spring_boot_c/domain/product/domain/entity/Product.java +++ b/src/main/java/study/spring_boot_c/domain/product/domain/entity/Product.java @@ -41,11 +41,11 @@ public class Product extends BaseEntity { private Category category; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "seller_id", nullable = false) private Member seller; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "buyer_id", nullable = false) private Member buyer; } diff --git a/src/main/java/study/spring_boot_c/global/config/RedisConfig.java b/src/main/java/study/spring_boot_c/global/config/RedisConfig.java new file mode 100644 index 0000000..301002d --- /dev/null +++ b/src/main/java/study/spring_boot_c/global/config/RedisConfig.java @@ -0,0 +1,38 @@ +package study.spring_boot_c.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import study.spring_boot_c.global.redis.RedisSubscriber; + +@Configuration +@RequiredArgsConstructor +public class RedisConfig { + + private final RedisConnectionFactory redisConnectionFactory; + private final RedisSubscriber redisSubscriber; + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); // JSON 문자열로 전송 + return template; + } + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer() { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(redisConnectionFactory); + container.addMessageListener(new MessageListenerAdapter(redisSubscriber, "onMessage"), new PatternTopic("chatroom:*")); + return container; + } +} + diff --git a/src/main/java/study/spring_boot_c/global/config/SecurityConfig.java b/src/main/java/study/spring_boot_c/global/config/SecurityConfig.java new file mode 100644 index 0000000..960f878 --- /dev/null +++ b/src/main/java/study/spring_boot_c/global/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package study.spring_boot_c.global.config; + +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.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/ws-chat/**").permitAll() // WebSocket 허용 + .anyRequest().permitAll() + ); + return http.build(); + } +} + diff --git a/src/main/java/study/spring_boot_c/global/config/WebSocketConfig.java b/src/main/java/study/spring_boot_c/global/config/WebSocketConfig.java new file mode 100644 index 0000000..9a49f8a --- /dev/null +++ b/src/main/java/study/spring_boot_c/global/config/WebSocketConfig.java @@ -0,0 +1,30 @@ +package study.spring_boot_c.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // 구독용 prefix (서버 → 클라이언트) + config.enableSimpleBroker("/topic"); + + // 클라이언트가 메시지 보낼 때 사용하는 prefix + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // WebSocket 엔드포인트 설정 (SockJS 지원) + registry.addEndpoint("/ws-chat") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} + diff --git a/src/main/java/study/spring_boot_c/global/error/code/status/ErrorStatus.java b/src/main/java/study/spring_boot_c/global/error/code/status/ErrorStatus.java index 51b9727..cb01846 100644 --- a/src/main/java/study/spring_boot_c/global/error/code/status/ErrorStatus.java +++ b/src/main/java/study/spring_boot_c/global/error/code/status/ErrorStatus.java @@ -16,9 +16,13 @@ public enum ErrorStatus implements BaseErrorCode { _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), //Member - NO_SUCH_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER_4001","멤버가 존재하지 않습니다.") + NO_SUCH_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER_4001","멤버가 존재하지 않습니다."), + //DB + DB_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "DB_5001", "데이터베이스 오류가 발생했습니다."), + //Redis + REDIS_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "REDIS_5001", "Redis 서버 오류가 발생했습니다.") ; private final HttpStatus httpStatus; diff --git a/src/main/java/study/spring_boot_c/global/error/code/status/SuccessStatus.java b/src/main/java/study/spring_boot_c/global/error/code/status/SuccessStatus.java index d939632..d638d6c 100644 --- a/src/main/java/study/spring_boot_c/global/error/code/status/SuccessStatus.java +++ b/src/main/java/study/spring_boot_c/global/error/code/status/SuccessStatus.java @@ -13,7 +13,10 @@ public enum SuccessStatus implements BaseCode { _CREATED(HttpStatus.CREATED, "COMMON201", "요청 성공 및 리소스 생성됨"), //member - MEMBER_EXAMPLE_SUCCESS(HttpStatus.OK,"MEMBER_200","성공적으로 조회되었습니다.") + MEMBER_EXAMPLE_SUCCESS(HttpStatus.OK,"MEMBER_200","성공적으로 조회되었습니다."), + + //chat + CHAT_SEND_SUCCESS(HttpStatus.OK,"CHAT_200","성공적으로 처리되었습니다.") ; diff --git a/src/main/java/study/spring_boot_c/global/redis/RedisPublisher.java b/src/main/java/study/spring_boot_c/global/redis/RedisPublisher.java new file mode 100644 index 0000000..a49299b --- /dev/null +++ b/src/main/java/study/spring_boot_c/global/redis/RedisPublisher.java @@ -0,0 +1,17 @@ +package study.spring_boot_c.global.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RedisPublisher { + + private final RedisTemplate redisTemplate; + + public void publish(String channel, String messageJson) { + redisTemplate.convertAndSend(channel, messageJson); + } +} + diff --git a/src/main/java/study/spring_boot_c/global/redis/RedisSubscriber.java b/src/main/java/study/spring_boot_c/global/redis/RedisSubscriber.java new file mode 100644 index 0000000..ad5c4bc --- /dev/null +++ b/src/main/java/study/spring_boot_c/global/redis/RedisSubscriber.java @@ -0,0 +1,38 @@ +package study.spring_boot_c.global.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import study.spring_boot_c.domain.chat.converter.ChatMessageConverter; +import study.spring_boot_c.domain.chat.domain.entity.ChatMessage; +import study.spring_boot_c.domain.chat.domain.repository.ChatMessageRepository; +import study.spring_boot_c.domain.chat.dto.ChatMessageDTO; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class RedisSubscriber implements MessageListener { + + private final SimpMessagingTemplate messagingTemplate; + private final ChatMessageRepository chatMessageRepository; + private final ObjectMapper objectMapper; + + public void onMessage(Message message, byte[] pattern) { + try { + String body = new String(message.getBody(), StandardCharsets.UTF_8); + ChatMessageDTO.MessageReceive chatMessage = objectMapper.readValue(body, ChatMessageDTO.MessageReceive.class); + + // 1. WebSocket으로 클라이언트에게 전송 + messagingTemplate.convertAndSend("/topic/chatroom/" + chatMessage.getRoomId(), chatMessage); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} +