diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 076149c..0ba1dd0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,8 +5,6 @@ name: Java CI with Gradle & Deploy to EC2 # develop 브랜치에 push가 되면 아래의 flow가 실행됨 on: # Triggers the workflow on push or pull request events but only for the "develop" branch - push: - branches: [ "develop" ] pull_request: branches: [ "develop" ] diff --git a/src/main/java/vom/spring/domain/album/AlbumService.java b/src/main/java/vom/spring/domain/album/AlbumService.java index a3f753e..8444b56 100644 --- a/src/main/java/vom/spring/domain/album/AlbumService.java +++ b/src/main/java/vom/spring/domain/album/AlbumService.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -41,19 +42,21 @@ public void setS3Client(AmazonS3Client amazonS3Client) { @Transactional public void uploadAlbum(Long memberId, MultipartFile multipartFile) throws IOException { Homepy homepy = homepyRepository.findByMember_id(memberId); + String originalFilename = multipartFile.getOriginalFilename(); + String uniqueFilename = UUID.randomUUID().toString(); ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(multipartFile.getSize()); metadata.setContentType(multipartFile.getContentType()); - amazonS3Client.putObject(bucket, originalFilename, multipartFile.getInputStream(), metadata); + amazonS3Client.putObject(bucket, uniqueFilename, multipartFile.getInputStream(), metadata); albumRepository.save( Album.builder() .homepy(homepy) .name(originalFilename) - .img_url(amazonS3Client.getUrl(bucket, originalFilename).toString()) + .img_url(amazonS3Client.getUrl(bucket, uniqueFilename).toString()) .createdAt(LocalDateTime.now()) .build() ); diff --git a/src/main/java/vom/spring/domain/webcam/controller/WebcamController.java b/src/main/java/vom/spring/domain/webcam/controller/WebcamController.java index c902730..d11bb87 100644 --- a/src/main/java/vom/spring/domain/webcam/controller/WebcamController.java +++ b/src/main/java/vom/spring/domain/webcam/controller/WebcamController.java @@ -16,10 +16,7 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.ErrorResponse; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import vom.spring.domain.webcam.domain.Message; import vom.spring.domain.webcam.dto.WebcamRequestDto; import vom.spring.domain.webcam.dto.WebcamResponseDto; @@ -28,15 +25,14 @@ import java.io.IOException; -@Tag(name = "화상채팅(시그널링) API", description = "유저 API 명세서") +@Tag(name = "화상채팅(시그널링) API", description = "화상채팅 API 명세서") @RestController @RequiredArgsConstructor @Slf4j -//db에 webcam 업데이트 로직 필요 public class WebcamController { private final WebcamServcie webcamServcie; - private final SimpMessagingTemplate messagingTemplate; private final FcmService fcmService; +// private final SimpMessagingTemplate messagingTemplate; /** * 방 생성 */ @@ -58,62 +54,61 @@ public ResponseEntity createWebcamRoom(@Reque * offer 정보를 주고받기 - step 5에서 offer를 받고 구독하고 있는 client들에게 전송 */ @MessageMapping("/peer/offer/{webcamId}") //해당 경로로 메시지가 날아오면 해당 메서드 실행해서 리턴, /app/~~이런식으로 전달된다 - //camKey : 각 요청하는 캠의 key , roomId : 룸 아이디 =>룸 id를 webcam id로 수정 - public void PeerHandleOffer(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) { - log.info("offer받은 webcamId:"+ message.getWebcamId()+"offer받은 sender:"+ message.getSender()); - messagingTemplate.convertAndSend("/topic/peer/offer/" +message.getWebcamId(), message); + @SendTo("/topic/peer/offer/{webcamId}") + public Message PeerHandleOffer(Message message, @DestinationVariable(value = "webcamId") String webcamId) { + log.info("offer 메세지 왔다, sender: {}, 전달할 webcamId는: {}", message.getSender(),message.getWebcamId()); +// messagingTemplate.convertAndSend("/topic/peer/offer/" +message.getWebcamId(), message); + return message; } /** * iceCandidate 정보를 주고 받기 위한 websocket */ @MessageMapping("/peer/iceCandidate/{webcamId}") - public void PeerHandleIceCandidate(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) { - log.info("ice받은 webcamId:"+ message.getWebcamId()+"ice받은 sender:"+ message.getSender()); - messagingTemplate.convertAndSend("/topic/peer/iceCandidate/" + message.getWebcamId(), message); + @SendTo("/topic/peer/iceCandidate/{webcamId}") + public Message PeerHandleIceCandidate(Message message, @DestinationVariable(value = "webcamId") String webcamId) { + log.info("[ICECANDIDATE] sender: {}, candidate 정보: {}, 전달할 webcamId: {}", message.getSender(), message.getIce(), message.getWebcamId()); +// messagingTemplate.convertAndSend("/topic/peer/iceCandidate/" + message.getWebcamId(), message); + return message; } /** * answer 정보 주고받기 */ @MessageMapping("/peer/answer/{webcamId}") - public void PeerHandleAnswer(@Payload Message message, SimpMessageHeaderAccessor simpMessageHeaderAccessor ) { - log.info("answer받은 webcamId:"+ message.getWebcamId()+"answer받은 sender:"+ message.getSender()); - messagingTemplate.convertAndSend("/topic/peer/answer/" + message.getWebcamId(), message); + @SendTo("/topic/peer/answer/{webcamId}") + public Message PeerHandleAnswer(Message message, @DestinationVariable(value = "webcamId") String webcamId) { + log.info("[ANSWER] sender: {}, 전달할 곳 : {} ", message.getSender(), message.getAnswer()); +// messagingTemplate.convertAndSend("/topic/peer/answer/" + message.getWebcamId(), message); + return message; } -// /** -// * camKey 를 받기위해 신호를 보내는 webSocket -// */ -// @Operation(summary = "camKey를 받기 위해 신호를 보냄", description = "client의 camKey 정보를 주고 받기 위해 신호를 보냅니다", -// responses = { -// @ApiResponse(responseCode = "201", description = "계정 생성 완료"), -// @ApiResponse(responseCode = "400", description = "존재하지 않은 직업, 존재하지 않은 주소", -// content = @Content(schema = @Schema(implementation = ErrorResponse.class))), -// @ApiResponse(responseCode = "409", description = "올바르지 않은 닉네임, 올바르지 않은 이메일", -// content = @Content(schema = @Schema(implementation = ErrorResponse.class))) -// }) -// @MessageMapping("/call/key") -// @SendTo("/topic/call/key") -// public String callKey(@Payload String message) { -// log.info("[Key] : {}", message); -// return message; -// } -// -// /** -// * 자신의 camKey 를 모든 연결된 세션에 보내는 webSocket -// */ -// @Operation(summary = "camKey를 모든 연결된 세션에 보냄", description = "client의 camKey를 연결된 모든 peer에게 보냅니다", -// responses = { -// @ApiResponse(responseCode = "200", description = "계정 생성 완료"), -// @ApiResponse(responseCode = "400", description = "존재하지 않은 직업, 존재하지 않은 주소", -// content = @Content(schema = @Schema(implementation = ErrorResponse.class))), -// @ApiResponse(responseCode = "409", description = "올바르지 않은 닉네임, 올바르지 않은 이메일", -// content = @Content(schema = @Schema(implementation = ErrorResponse.class))) -// }) -// @MessageMapping("/send/key") -// @SendTo("/topic/send/key") -// public String sendKey(@Payload String message) { -// return message; -// } + /** + * leave 정보 주고받기 + */ + @MessageMapping("/peer/leaveRoom/{webcamId}") + @SendTo("/topic/peer/leaveRoom/{webcamId}") + public Message PeerHandleLeave(Message message, @DestinationVariable(value = "webcamId") String webcamId) { + log.info("[LEAVE] sender: {}, 전달할 곳 : {} ", message.getSender(), message.getWebcamId()); +// messagingTemplate.convertAndSend("/topic/peer/answer/" + message.getWebcamId(), message); + return message; + } + + /** + * 방 삭제 + */ + @Operation(summary = "화상채팅 방을 삭제합니다", description = "화상채팅 방을 삭제합니다", + responses = { + @ApiResponse(responseCode = "200", description = "화상채팅 방을 삭제했습니다."), + @ApiResponse(responseCode = "400", description = "채팅 방을 삭제하지 못했습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "채팅 방을 찾지 못했습니다", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/api/webcam") + public ResponseEntity deleteWebcamRoom(@RequestBody WebcamRequestDto.DeleteWebcamDto request) { + System.out.print("방id: " + request.getRoomId()); + webcamServcie.deleteWebcamRoom(request); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/src/main/java/vom/spring/domain/webcam/domain/Type.java b/src/main/java/vom/spring/domain/webcam/domain/Type.java index b0cf1c9..5427f79 100644 --- a/src/main/java/vom/spring/domain/webcam/domain/Type.java +++ b/src/main/java/vom/spring/domain/webcam/domain/Type.java @@ -1,5 +1,5 @@ package vom.spring.domain.webcam.domain; public enum Type { - OFFER, ANSWER, ENTER, ICE + OFFER, ANSWER, ENTER, ICE, LEAVE } diff --git a/src/main/java/vom/spring/domain/webcam/dto/WebcamRequestDto.java b/src/main/java/vom/spring/domain/webcam/dto/WebcamRequestDto.java index f820180..80e3cb1 100644 --- a/src/main/java/vom/spring/domain/webcam/dto/WebcamRequestDto.java +++ b/src/main/java/vom/spring/domain/webcam/dto/WebcamRequestDto.java @@ -10,4 +10,12 @@ public class WebcamRequestDto { public static class CreateWebcamDto { private Long toMemberId; //화상 채팅 요청받은 유저의 id } + + @Builder + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class DeleteWebcamDto { + private Long roomId; + } } diff --git a/src/main/java/vom/spring/domain/webcam/repository/MemberWebcamRepository.java b/src/main/java/vom/spring/domain/webcam/repository/MemberWebcamRepository.java index 34f2966..a0844e9 100644 --- a/src/main/java/vom/spring/domain/webcam/repository/MemberWebcamRepository.java +++ b/src/main/java/vom/spring/domain/webcam/repository/MemberWebcamRepository.java @@ -2,7 +2,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import vom.spring.domain.webcam.domain.MemberWebcam; +import vom.spring.domain.webcam.domain.Webcam; + +import java.util.List; +import java.util.Optional; public interface MemberWebcamRepository extends JpaRepository { - + void deleteByWebcam(Webcam webcam); } diff --git a/src/main/java/vom/spring/domain/webcam/repository/WebcamRepository.java b/src/main/java/vom/spring/domain/webcam/repository/WebcamRepository.java index 9c55ba5..a3dde98 100644 --- a/src/main/java/vom/spring/domain/webcam/repository/WebcamRepository.java +++ b/src/main/java/vom/spring/domain/webcam/repository/WebcamRepository.java @@ -3,5 +3,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import vom.spring.domain.webcam.domain.Webcam; +import java.util.Optional; + public interface WebcamRepository extends JpaRepository { + + Optional findById(Long webcamId); + + void deleteById(Long webcamId); } diff --git a/src/main/java/vom/spring/domain/webcam/service/WebcamServcie.java b/src/main/java/vom/spring/domain/webcam/service/WebcamServcie.java index 570500f..a72aa22 100644 --- a/src/main/java/vom/spring/domain/webcam/service/WebcamServcie.java +++ b/src/main/java/vom/spring/domain/webcam/service/WebcamServcie.java @@ -5,4 +5,5 @@ public interface WebcamServcie { WebcamResponseDto.CreateWebcamDto createWebcamRoom(WebcamRequestDto.CreateWebcamDto request); + void deleteWebcamRoom(WebcamRequestDto.DeleteWebcamDto request); } diff --git a/src/main/java/vom/spring/domain/webcam/service/WebcamServiceImpl.java b/src/main/java/vom/spring/domain/webcam/service/WebcamServiceImpl.java index a8ff246..f8ba1d8 100644 --- a/src/main/java/vom/spring/domain/webcam/service/WebcamServiceImpl.java +++ b/src/main/java/vom/spring/domain/webcam/service/WebcamServiceImpl.java @@ -42,4 +42,17 @@ public WebcamResponseDto.CreateWebcamDto createWebcamRoom(WebcamRequestDto.Creat memberWebcamRepository.save(toMemberWebcam); return WebcamResponseDto.CreateWebcamDto.builder().webcamId(newWebcam.getId()).build(); } + + @Transactional + @Override + public void deleteWebcamRoom(WebcamRequestDto.DeleteWebcamDto request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); //현재 접속 유저 정보 가져오기 + Member fromMember = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("존재하지 않은 유저입니다")); + //해당 방 찾기 + Webcam webcam = webcamRepository.findById(request.getRoomId()).orElseThrow(() -> new IllegalArgumentException("존재하지 않은 방입니다")); + //해당 방 관련 연관관계 삭제 + memberWebcamRepository.deleteByWebcam(webcam); + //해당 방 삭제 + webcamRepository.deleteById(webcam.getId()); + } } diff --git a/src/main/java/vom/spring/global/config/StompPreHandler.java b/src/main/java/vom/spring/global/config/StompPreHandler.java index f067789..0309ce2 100644 --- a/src/main/java/vom/spring/global/config/StompPreHandler.java +++ b/src/main/java/vom/spring/global/config/StompPreHandler.java @@ -1,81 +1,81 @@ -//package vom.spring.global.config; -// -//import com.fasterxml.jackson.core.JsonProcessingException; -//import io.jsonwebtoken.MalformedJwtException; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.messaging.Message; -//import org.springframework.messaging.MessageChannel; -//import org.springframework.messaging.MessageDeliveryException; -//import org.springframework.messaging.simp.stomp.StompCommand; -//import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -//import org.springframework.messaging.support.ChannelInterceptor; -//import org.springframework.messaging.support.MessageHeaderAccessor; -//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -//import org.springframework.security.core.Authentication; -//import org.springframework.security.core.context.SecurityContextHolder; -//import vom.spring.global.jwt.JwtTokenProvider; -// -//@Configuration -//@RequiredArgsConstructor -//@Slf4j -//public class StompPreHandler implements ChannelInterceptor { -// -// private final JwtTokenProvider jwtTokenProvider; -// private String token; -// private Long memberId = 0L; -// @Override -// public Message preSend(Message message, MessageChannel channel) { -// try { -// StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); -// String authorizationHeader = String.valueOf(headerAccessor.getNativeHeader("Authorization")); -// StompCommand command = headerAccessor.getCommand(); -// if (command.equals(StompCommand.UNSUBSCRIBE) || command.equals(StompCommand.MESSAGE) || command.equals(StompCommand.CONNECTED) || command.equals(StompCommand.SEND)) -// return message; -// else if (command.equals(StompCommand.ERROR)) { -// log.error("메시지 에러"); -// throw new MessageDeliveryException("error"); -// } -// if (authorizationHeader == null) { -// log.info("chat header가 없는 요청입니다."); -// throw new MalformedJwtException("jwt"); -// } -// // 토큰 추출 -// token = ""; -// String authorizationHeaderStr = authorizationHeader.replace("[","").replace("]",""); -// if (authorizationHeaderStr.startsWith("Bearer ")) { -// token = authorizationHeaderStr.replace("Bearer ", ""); -// } else { -// log.error("Authorization 헤더 형식이 틀립니다. : {}", authorizationHeader); -// throw new MalformedJwtException("jwt"); -// } -// //memberId 추출 -// try { -// memberId = jwtTokenProvider.getMemberId(token); -// -// } catch (Exception e) { -// throw new MalformedJwtException("stomp_jwt"); -// } -// //토큰 유효검증 -// if (jwtTokenProvider.validateAccessToken(token)) {//access token 검증 -// setAuthentication(message, headerAccessor); -// log.info("인증 성공"); -// } -// } catch (Exception e) { -// log.error("JWT에러"); -// throw new MalformedJwtException("jwt"); -// } -// return message; -// } -// -// /** -// * Security ContextHolder에 인증 정보 담아두기 -// */ -// private void setAuthentication(Message message, StompHeaderAccessor headerAccessor) { -//// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(memberId, null, List.of(new SimpleGrantedAuthority(MemberRole.USER.name()))); -// Authentication authentication = jwtTokenProvider.getAuthentication(token); -// SecurityContextHolder.getContext().setAuthentication(authentication); -// headerAccessor.setUser(authentication); -// } -//} +package vom.spring.global.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.jsonwebtoken.MalformedJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import vom.spring.global.jwt.JwtTokenProvider; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class StompPreHandler implements ChannelInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + private String token; + private Long memberId = 0L; + @Override + public Message preSend(Message message, MessageChannel channel) { + try { + StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + String authorizationHeader = String.valueOf(headerAccessor.getNativeHeader("Authorization")); + StompCommand command = headerAccessor.getCommand(); + if (command.equals(StompCommand.UNSUBSCRIBE) || command.equals(StompCommand.MESSAGE) || command.equals(StompCommand.CONNECTED) || command.equals(StompCommand.SEND)) + return message; + else if (command.equals(StompCommand.ERROR)) { + log.error("메시지 에러"); + throw new MessageDeliveryException("error"); + } + if (authorizationHeader == null) { + log.info("chat header가 없는 요청입니다."); + throw new MalformedJwtException("jwt"); + } + // 토큰 추출 + token = ""; + String authorizationHeaderStr = authorizationHeader.replace("[","").replace("]",""); + if (authorizationHeaderStr.startsWith("Bearer ")) { + token = authorizationHeaderStr.replace("Bearer ", ""); + } else { + log.error("Authorization 헤더 형식이 틀립니다. : {}", authorizationHeader); + throw new MalformedJwtException("jwt"); + } + //memberId 추출 + try { + memberId = jwtTokenProvider.getMemberId(token); + + } catch (Exception e) { + throw new MalformedJwtException("stomp_jwt"); + } + //토큰 유효검증 + if (jwtTokenProvider.validateAccessToken(token)) {//access token 검증 + setAuthentication(message, headerAccessor); + log.info("인증 성공"); + } + } catch (Exception e) { + log.error("JWT에러"); + throw new MalformedJwtException("jwt"); + } + return message; + } + + /** + * Security ContextHolder에 인증 정보 담아두기 + */ + private void setAuthentication(Message message, StompHeaderAccessor headerAccessor) { +// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(memberId, null, List.of(new SimpleGrantedAuthority(MemberRole.USER.name()))); + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + headerAccessor.setUser(authentication); + } +} diff --git a/src/main/java/vom/spring/global/config/WebConfig.java b/src/main/java/vom/spring/global/config/WebConfig.java index 650e6cd..d13f264 100644 --- a/src/main/java/vom/spring/global/config/WebConfig.java +++ b/src/main/java/vom/spring/global/config/WebConfig.java @@ -39,6 +39,9 @@ public void addInterceptors(InterceptorRegistry registry) { "/api/auth/**", "/api/members/join", "/api/members/join/nickname", + "/topic/**", + "/app/**", + "/signaling", "/", "/swagger-ui/index.html", "/swagger-ui.html",