Skip to content

Commit

Permalink
Merge pull request #95 from BOOK-TALK/#71-stomp
Browse files Browse the repository at this point in the history
#71 stomp
  • Loading branch information
chanwoo7 authored Aug 23, 2024
2 parents 9fc4c64 + 4452747 commit e91148b
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.book.backend.domain.message.controller;

import com.book.backend.domain.message.dto.MessageRequestDto;
import com.book.backend.domain.message.dto.MessageResponseDto;
import com.book.backend.domain.message.entity.Message;
import com.book.backend.domain.message.service.MessageService;
import com.book.backend.global.ResponseTemplate;
import com.book.backend.global.log.RequestLogger;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
@RequiredArgsConstructor
@Slf4j
@Tag(name="채팅", description = "메세지 저장 / 메세지 불러오기")
public class MessageController {
private final MessageService messageService;
private final ResponseTemplate responseTemplate;
private final SimpMessageSendingOperations sendingOperations;

// 채팅 저장하기 (apic 으로 테스트)
@MessageMapping("/message")
public void chat(MessageRequestDto messageRequestDto) {
RequestLogger.param(new String[]{"messageRequestDto"}, messageRequestDto);
MessageResponseDto response = messageService.saveMessage(messageRequestDto);
sendingOperations.convertAndSend("/sub/message/" + messageRequestDto.getOpentalkId(), response); // 수신자들에게 전송
}


// 채팅 불러오기
@Operation(summary="채팅 불러오기 (특정 오픈톡)", description="오픈톡 ID 를 입력으로 받아 pageSize개 데이터를 반환합니다. (pageNo로 페이지네이션)",
parameters = {@Parameter(name = "opentalkId", description = "오픈톡 DB ID"), @Parameter(name = "pageNo", description = "페이지 번호(0부터)"), @Parameter(name = "pageSize", description = "페이지 당 개수")},
responses = {@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MessageResponseDto.class)),
description = MessageResponseDto.description)})
@GetMapping("/api/message/get")
public ResponseEntity<?> getChat(@RequestParam String opentalkId, int pageNo, int pageSize) {
RequestLogger.param(new String[]{"opentalkId, pageNo, pageSize"}, opentalkId, pageNo, pageSize);

Pageable pageRequest = PageRequest.of(pageNo, pageSize, Sort.by("createdAt").descending());
Page<Message> MessagePage = messageService.getMessage(opentalkId, pageRequest);
List<MessageResponseDto> response = messageService.pageToDto(MessagePage);

return responseTemplate.success(response, HttpStatus.OK);
}

// swagger docs 에 남기기 위한 용도
@Operation(summary="채팅 stomp 통신", description="APIC 테스터기를 이용해서 stomp 통신을 합니다. \n" +
"- Request URL: ws://52.79.187.133:8080/ws-stomp \n"+
"- Destination Queue: /pub/message \n"+
"- Subscription URL: /sub/message/{opentalkId}",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MessageRequestDto.class))),
responses = {@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MessageResponseDto.class)),
description = MessageResponseDto.description)})
@PostMapping("")
public void chatForSwagger(MessageRequestDto messageRequestDto) {
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
public class MessageRequestDto {
// private String type; //text, img
private String jwtToken;
private Long opentalkId;
private String content;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

Expand All @@ -24,20 +25,24 @@ public class MessageMapper {
private final UserRepository userRepository;
private final OpentalkRepository opentalkRepository;

public Message convertToMessage(MessageRequestDto messageRequestDto) {
Message message = mapper.map(messageRequestDto, Message.class);
@Transactional
public Message convertToMessage(MessageRequestDto dto) {
User user = userService.loadLoggedinUser();
if (user == null) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}
Opentalk opentalk = opentalkRepository.findById(messageRequestDto.getOpentalkId()).orElseThrow();
String content = dto.getContent();
Opentalk opentalk = opentalkRepository.findById(dto.getOpentalkId()).orElseThrow();

message.setUser(user);
Message message = new Message();
message.setUser(user); // 보낸 사람
message.setOpentalk(opentalk);
message.setContent(content);
message.setCreatedAt(new Date());

return message;
}

public MessageResponseDto convertToMessageResponseDto(Message message) {
User user = userRepository.findByLoginId(message.getUser().getLoginId()).orElseThrow();
return MessageResponseDto.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.book.backend.domain.message.service;

import com.book.backend.domain.auth.service.CustomUserDetailsService;
import com.book.backend.domain.message.dto.MessageRequestDto;
import com.book.backend.domain.message.dto.MessageResponseDto;
import com.book.backend.domain.message.entity.Message;
import com.book.backend.domain.message.mapper.MessageMapper;
import com.book.backend.domain.message.repository.MessageRepository;
import com.book.backend.domain.opentalk.entity.Opentalk;
import com.book.backend.domain.opentalk.repository.OpentalkRepository;
import com.book.backend.exception.CustomException;
import com.book.backend.exception.ErrorCode;
import com.book.backend.util.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.LinkedList;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class MessageService {
private final MessageRepository messageRepository;
private final OpentalkRepository opentalkRepository;
private final MessageMapper messageMapper;
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;

@Transactional
public MessageResponseDto saveMessage(MessageRequestDto messageRequestDto){
log.trace("MessageService > saveMessage()");
// 토큰 유효성 검사
String token = messageRequestDto.getJwtToken();
validateToken(token);

// message DB에 저장
Message message = messageMapper.convertToMessage(messageRequestDto);
try{
messageRepository.save(message);
} catch (Exception e){
throw new CustomException(ErrorCode.MESSAGE_SAVE_FAILED);
}
return messageMapper.convertToMessageResponseDto(message);
}


public void validateToken(String token) {
try{
String username = jwtUtil.getUsernameFromToken(token); // username 가져옴
// 현재 SecurityContextHolder에 인증객체가 있는지 확인
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByloginId(username);
} catch (CustomException e1) {
try {
userDetails = userDetailsService.loadUserByKakaoId(username);
} catch (CustomException e2) {
userDetails = userDetailsService.loadUserByAppleId(username);
}
}

// 토큰 유효성 검증
if (jwtUtil.isValidToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authenticated
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticated);
}
}
} catch (ExpiredJwtException e) {
throw new CustomException(ErrorCode.JWT_EXPIRED);
} catch (Exception e) {
throw new CustomException(ErrorCode.WRONG_JWT_TOKEN);
}
}

public Page<Message> getMessage(String opentalkId, Pageable pageRequest){
log.trace("MessageService > getOpentalkMessage()");
// 오픈톡 ID로 opentlak 객체 찾기
Opentalk opentalk = opentalkRepository.findByOpentalkId(Long.parseLong(opentalkId)).orElseThrow(() -> new CustomException(ErrorCode.OPENTALK_NOT_FOUND));
return messageRepository.findAllByOpentalk(opentalk, pageRequest);
}

public List<MessageResponseDto> pageToDto(Page<Message> page){
log.trace("MessageService > pageToDto()");
List<Message> messages = page.getContent();
List<MessageResponseDto> messageList = new LinkedList<>();

for(Message message : messages){
messageList.add(messageMapper.convertToMessageResponseDto(message));
}
return messageList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
import com.book.backend.domain.opentalk.dto.OpentalkDto;
import com.book.backend.domain.opentalk.dto.OpentalkJoinResponseDto;
import com.book.backend.domain.opentalk.dto.OpentalkResponseDto;
import com.book.backend.domain.message.dto.MessageRequestDto;
import com.book.backend.domain.message.dto.MessageResponseDto;
import com.book.backend.domain.message.entity.Message;
import com.book.backend.domain.opentalk.service.OpentalkService;
import com.book.backend.global.ResponseTemplate;
import com.book.backend.global.log.RequestLogger;
Expand All @@ -19,10 +16,6 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand Down Expand Up @@ -57,32 +50,6 @@ public ResponseEntity<?> opentalkMain() throws Exception {
return responseTemplate.success(response, HttpStatus.OK);
}

// 채팅 불러오기
@Operation(summary="특정 오픈톡 채팅 불러오기", description="오픈톡 ID 를 입력으로 받아 pageSize개 데이터를 반환합니다. (pageNo로 페이지네이션)",
parameters = {@Parameter(name = "opentalkId", description = "오픈톡 DB ID"), @Parameter(name = "pageNo", description = "페이지 번호(0부터)"), @Parameter(name = "pageSize", description = "페이지 당 개수")},
responses = {@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MessageResponseDto.class)),
description = MessageResponseDto.description)})
@GetMapping("/chat/get")
public ResponseEntity<?> getChat(@RequestParam String opentalkId, int pageNo, int pageSize) {
RequestLogger.param(new String[]{"opentalkId, pageNo, pageSize"}, opentalkId, pageNo, pageSize);

Pageable pageRequest = PageRequest.of(pageNo, pageSize, Sort.by("createdAt").descending());
Page<Message> MessagePage = opentalkService.getOpentalkMessage(opentalkId, pageRequest);
List<MessageResponseDto> response = opentalkService.pageToDto(MessagePage);

return responseTemplate.success(response, HttpStatus.OK);
}

// 채팅 저장하기
@Operation(summary="채팅 저장", description="오픈톡 DB ID, content 를 입력으로 받아 DB에 채팅을 저장합니다.",
responses = {@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MessageResponseDto.class)),
description = MessageResponseDto.description)})
@PostMapping("/chat/save")
public ResponseEntity<?> saveChat(@RequestBody MessageRequestDto messageRequestDto) {
RequestLogger.param(new String[]{"messageRequestDto"}, messageRequestDto);
MessageResponseDto response = opentalkService.saveMessage(messageRequestDto);
return responseTemplate.success(response, HttpStatus.OK);
}

// [오픈톡 참여하기]
@Operation(summary="오픈톡 참여하기", description="isbn, pageSize를 입력으로 받아, 오픈톡 ID, 채팅 내역 반환",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.book.backend.domain.message.entity.Message;
import com.book.backend.domain.message.mapper.MessageMapper;
import com.book.backend.domain.message.repository.MessageRepository;
import com.book.backend.domain.message.service.MessageService;
import com.book.backend.domain.openapi.dto.request.DetailRequestDto;
import com.book.backend.domain.openapi.dto.response.DetailResponseDto;
import com.book.backend.domain.opentalk.dto.OpentalkDto;
Expand Down Expand Up @@ -46,7 +47,7 @@ public class OpentalkService {
private final DetailService detailService;
private final UserService userService;
private final OpentalkResponseParser opentalkResponseParser;
private final MessageMapper messageMapper;
private final MessageService messageService;

/* message 테이블에서 최근 200개 데이터 조회 -> opentalkId 기준으로 count 해서 가장 빈번하게 나오는 top 5 id 반환*/
public List<Long> hotOpentalk() {
Expand Down Expand Up @@ -97,35 +98,6 @@ public List<OpentalkDto> getBookInfo(List<Long> opentalkId) throws Exception {
return opentalkDtoList;
}

public Page<Message> getOpentalkMessage(String opentalkId, Pageable pageRequest){
log.trace("OpentalkService > getOpentalkMessage()");
// 오픈톡 ID로 opentlak 객체 찾기
Opentalk opentalk = opentalkRepository.findByOpentalkId(Long.parseLong(opentalkId)).orElseThrow(() -> new CustomException(ErrorCode.OPENTALK_NOT_FOUND));
return messageRepository.findAllByOpentalk(opentalk, pageRequest);
}

public List<MessageResponseDto> pageToDto(Page<Message> page){
log.trace("OpentalkService > pageToDto()");
List<Message> messages = page.getContent();
List<MessageResponseDto> messageList = new LinkedList<>();

for(Message message : messages){
messageList.add(messageMapper.convertToMessageResponseDto(message));
}
return messageList;
}

// message 저장
public MessageResponseDto saveMessage(MessageRequestDto messageRequestDto){
log.trace("OpentalkService > saveMessage()");
Message message = messageMapper.convertToMessage(messageRequestDto);
try{
messageRepository.save(message);
} catch (Exception e){
throw new CustomException(ErrorCode.MESSAGE_SAVE_FAILED);
}
return messageMapper.convertToMessageResponseDto(message);
}

// 오픈톡 참여하기
@Transactional
Expand All @@ -137,8 +109,8 @@ public OpentalkJoinResponseDto joinOpentalk(String isbn, int pageSize){
return OpentalkJoinResponseDto.builder().opentalkId(opentalkId).messageResponseDto(null).build();
}
Pageable pageRequest = PageRequest.of(0, pageSize, Sort.by("createdAt").descending());
Page<Message> messagePage = getOpentalkMessage(opentalkId.toString(), pageRequest);
List<MessageResponseDto> response = pageToDto(messagePage);
Page<Message> messagePage = messageService.getMessage(opentalkId.toString(), pageRequest);
List<MessageResponseDto> response = messageService.pageToDto(messagePage);
return OpentalkJoinResponseDto.builder().opentalkId(opentalkId).messageResponseDto(response).build();
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/book/backend/global/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
.requestMatchers("/login/oauth2/**").permitAll() // OAuth2 Callback 경로
.requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/kakaoLogin", "/api/auth/appleLogin").permitAll() // 회원가입, 로그인 경로
.requestMatchers("/.well-known/**").permitAll()
// .requestMatchers("/api/**") // 모든 API에 대한 인증 비활성화 (개발용)
.requestMatchers("/ws-stomp/**").permitAll() // stomp 통신
// .requestMatchers("/**").permitAll() // 모든 API에 대한 인증 비활성화 (개발용)
.anyRequest().authenticated()
)
.exceptionHandling((exception) -> exception
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/book/backend/global/stomp/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.book.backend.global.stomp;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
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
@EnableWebSocket
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/*
request URL = ws://localhost:8080/ws-stomp
Destination (발신 URL) = /pub/message
Subscription (수신 URL) = /sub/message/{opentalkId}
*/
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp")
.setAllowedOriginPatterns("*");
}

@Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub"); // 발행요청
registry.enableSimpleBroker("/sub"); // 구독요청
}
}

0 comments on commit e91148b

Please sign in to comment.