diff --git a/build.gradle b/build.gradle index 9e26a314..ab259595 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,9 @@ dependencies { // SES implementation 'com.amazonaws:aws-java-sdk-ses:1.12.778' + //S3 + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.777' + // IOT implementation 'com.amazonaws:aws-java-sdk-iot:1.12.778' implementation 'software.amazon.awssdk.iotdevicesdk:aws-iot-device-sdk:1.21.0' diff --git a/src/main/java/org/ioteatime/meonghanyangserver/clients/s3/S3Client.java b/src/main/java/org/ioteatime/meonghanyangserver/clients/s3/S3Client.java new file mode 100644 index 00000000..6b39520a --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/clients/s3/S3Client.java @@ -0,0 +1,32 @@ +package org.ioteatime.meonghanyangserver.clients.s3; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import java.net.URL; +import java.util.Calendar; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class S3Client { + private final AmazonS3 amazonS3; + + @Value("${aws.s3-bucket}") + private String bucket; + + public String generatePreSignUrl(String filename, HttpMethod httpMethod) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.MINUTE, 10); + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucket, filename) + .withMethod(httpMethod) + .withExpiration(calendar.getTime()); + URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest); + return url.toString(); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoErrorType.java b/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoErrorType.java new file mode 100644 index 00000000..adfc1d83 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoErrorType.java @@ -0,0 +1,23 @@ +package org.ioteatime.meonghanyangserver.common.type; + +public enum VideoErrorType implements ErrorTypeCode { + VIDEO_NOT_FOUND("VIDEO NOT FOUND", "동영상 정보를 찾을 수 없습니다."); + + private final String message; + private final String description; + + VideoErrorType(String message, String description) { + this.message = message; + this.description = description; + } + + @Override + public String getMessage() { + return this.message; + } + + @Override + public String getDescription() { + return this.description; + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoSuccessType.java b/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoSuccessType.java new file mode 100644 index 00000000..21dd13f1 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/common/type/VideoSuccessType.java @@ -0,0 +1,30 @@ +package org.ioteatime.meonghanyangserver.common.type; + +public enum VideoSuccessType implements SuccessTypeCode { + GET_PRESIGNED_URL(200, "OK", "Presigned Url 조회에 성공하였습니다."); + + private final Integer code; + private final String message; + private final String description; + + VideoSuccessType(Integer code, String message, String description) { + this.code = code; + this.message = message; + this.description = description; + } + + @Override + public Integer getCode() { + return this.code; + } + + @Override + public String getMessage() { + return this.message; + } + + @Override + public String getDescription() { + return this.description; + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/config/AwsConfig.java b/src/main/java/org/ioteatime/meonghanyangserver/config/AwsConfig.java index e91de599..a23805fd 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/config/AwsConfig.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/config/AwsConfig.java @@ -8,6 +8,8 @@ import com.amazonaws.services.iot.model.*; import com.amazonaws.services.kinesisvideo.AmazonKinesisVideo; import com.amazonaws.services.kinesisvideo.AmazonKinesisVideoClient; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import lombok.RequiredArgsConstructor; @@ -27,6 +29,26 @@ public class AwsConfig { private final AwsProperties awsProperties; + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = + new AWSCredentials() { + @Override + public String getAWSAccessKeyId() { + return awsProperties.s3AccessKey(); + } + + @Override + public String getAWSSecretKey() { + return awsProperties.s3SecretKey(); + } + }; + return AmazonS3Client.builder() + .withRegion(Regions.AP_NORTHEAST_2) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + @Bean public AmazonKinesisVideo amazonKinesisVideo() { AWSCredentials awsCredentials = diff --git a/src/main/java/org/ioteatime/meonghanyangserver/config/AwsProperties.java b/src/main/java/org/ioteatime/meonghanyangserver/config/AwsProperties.java index ef146d63..bc699cb8 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/config/AwsProperties.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/config/AwsProperties.java @@ -11,4 +11,6 @@ public record AwsProperties( String iotAccessKey, String iotSecretKey, String iotEndpoint, - String iotClientId) {} + String iotClientId, + String s3SecretKey, + String s3AccessKey) {} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoApi.java b/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoApi.java new file mode 100644 index 00000000..f58a546e --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoApi.java @@ -0,0 +1,14 @@ +package org.ioteatime.meonghanyangserver.video.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.ioteatime.meonghanyangserver.common.api.Api; +import org.ioteatime.meonghanyangserver.common.utils.LoginMember; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "Video Api", description = "Video 관련 API 목록입니다.") +public interface VideoApi { + @Operation(summary = "동영상의 presigned url을 발급받습니다.", description = "담당자: 최민석") + public Api getVideoPresignedUrl( + @LoginMember Long memberId, @PathVariable Long videoId, @PathVariable Long groupId); +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoController.java b/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoController.java new file mode 100644 index 00000000..25f6467c --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/controller/VideoController.java @@ -0,0 +1,27 @@ +package org.ioteatime.meonghanyangserver.video.controller; + +import lombok.RequiredArgsConstructor; +import org.ioteatime.meonghanyangserver.common.api.Api; +import org.ioteatime.meonghanyangserver.common.type.VideoSuccessType; +import org.ioteatime.meonghanyangserver.common.utils.LoginMember; +import org.ioteatime.meonghanyangserver.video.dto.response.VideoPresignedUrlResponse; +import org.ioteatime.meonghanyangserver.video.service.VideoService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/video") +public class VideoController implements VideoApi { + private final VideoService videoService; + + @GetMapping("/{videoId}/group/{groupId}") + public Api getVideoPresignedUrl( + @LoginMember Long memberId, @PathVariable Long videoId, @PathVariable Long groupId) { + VideoPresignedUrlResponse videoPresignedUrlResponse = + videoService.getVideoPresignedUrl(memberId, videoId, groupId); + return Api.success(VideoSuccessType.GET_PRESIGNED_URL, videoPresignedUrlResponse); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/entity/VideoEntity.java b/src/main/java/org/ioteatime/meonghanyangserver/video/domain/VideoEntity.java similarity index 91% rename from src/main/java/org/ioteatime/meonghanyangserver/video/entity/VideoEntity.java rename to src/main/java/org/ioteatime/meonghanyangserver/video/domain/VideoEntity.java index b780a0a7..a2f19320 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/video/entity/VideoEntity.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/domain/VideoEntity.java @@ -1,4 +1,4 @@ -package org.ioteatime.meonghanyangserver.video.entity; +package org.ioteatime.meonghanyangserver.video.domain; import jakarta.persistence.*; import java.time.LocalDateTime; diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/dto/response/VideoPresignedUrlResponse.java b/src/main/java/org/ioteatime/meonghanyangserver/video/dto/response/VideoPresignedUrlResponse.java new file mode 100644 index 00000000..c932a77e --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/dto/response/VideoPresignedUrlResponse.java @@ -0,0 +1,12 @@ +package org.ioteatime.meonghanyangserver.video.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "동영상 presigned url 조회 응답") +public record VideoPresignedUrlResponse( + @NotBlank + @Schema( + description = "동영상 presigned url", + example = "https://bucket.s3.ap-northeast-2.amazonaws.com/test?") + String presignedUrl) {} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/mapper/VideoResponseMapper.java b/src/main/java/org/ioteatime/meonghanyangserver/video/mapper/VideoResponseMapper.java new file mode 100644 index 00000000..8da964a4 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/mapper/VideoResponseMapper.java @@ -0,0 +1,9 @@ +package org.ioteatime.meonghanyangserver.video.mapper; + +import org.ioteatime.meonghanyangserver.video.dto.response.VideoPresignedUrlResponse; + +public class VideoResponseMapper { + public static VideoPresignedUrlResponse form(String presignedURL) { + return new VideoPresignedUrlResponse(presignedURL); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/repository/JpaVideoRepository.java b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/JpaVideoRepository.java new file mode 100644 index 00000000..3ca924d3 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/JpaVideoRepository.java @@ -0,0 +1,6 @@ +package org.ioteatime.meonghanyangserver.video.repository; + +import org.ioteatime.meonghanyangserver.video.domain.VideoEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaVideoRepository extends JpaRepository {} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepository.java b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepository.java new file mode 100644 index 00000000..ee2d3e40 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepository.java @@ -0,0 +1,8 @@ +package org.ioteatime.meonghanyangserver.video.repository; + +import java.util.Optional; +import org.ioteatime.meonghanyangserver.video.domain.VideoEntity; + +public interface VideoRepository { + Optional findById(Long videoId); +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepositoryImpl.java b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepositoryImpl.java new file mode 100644 index 00000000..773a285b --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/repository/VideoRepositoryImpl.java @@ -0,0 +1,19 @@ +package org.ioteatime.meonghanyangserver.video.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.ioteatime.meonghanyangserver.video.domain.VideoEntity; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class VideoRepositoryImpl implements VideoRepository { + private final JpaVideoRepository jpaVideoRepository; + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Optional findById(Long videoId) { + return jpaVideoRepository.findById(videoId); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/video/service/VideoService.java b/src/main/java/org/ioteatime/meonghanyangserver/video/service/VideoService.java new file mode 100644 index 00000000..140d97a0 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/video/service/VideoService.java @@ -0,0 +1,36 @@ +package org.ioteatime.meonghanyangserver.video.service; + +import com.amazonaws.HttpMethod; +import lombok.RequiredArgsConstructor; +import org.ioteatime.meonghanyangserver.clients.s3.S3Client; +import org.ioteatime.meonghanyangserver.common.exception.NotFoundException; +import org.ioteatime.meonghanyangserver.common.type.GroupErrorType; +import org.ioteatime.meonghanyangserver.common.type.VideoErrorType; +import org.ioteatime.meonghanyangserver.groupmember.repository.GroupMemberRepository; +import org.ioteatime.meonghanyangserver.video.domain.VideoEntity; +import org.ioteatime.meonghanyangserver.video.dto.response.VideoPresignedUrlResponse; +import org.ioteatime.meonghanyangserver.video.mapper.VideoResponseMapper; +import org.ioteatime.meonghanyangserver.video.repository.VideoRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VideoService { + private final VideoRepository videoRepository; + private final GroupMemberRepository groupMemberRepository; + private final S3Client s3Client; + + public VideoPresignedUrlResponse getVideoPresignedUrl( + Long memberId, Long videoId, Long groupId) { + if (!groupMemberRepository.existsByMemberIdAndGroupId(memberId, groupId)) { + throw new NotFoundException(GroupErrorType.GROUP_MEMBER_NOT_FOUND); + } + VideoEntity videoEntity = + videoRepository + .findById(videoId) + .orElseThrow(() -> new NotFoundException(VideoErrorType.VIDEO_NOT_FOUND)); + String presignedURL = + s3Client.generatePreSignUrl(videoEntity.getVideoName(), HttpMethod.GET); + return VideoResponseMapper.form(presignedURL); + } +} diff --git a/src/main/resources/application-key.yaml b/src/main/resources/application-key.yaml index a584394d..4ac0fa57 100644 --- a/src/main/resources/application-key.yaml +++ b/src/main/resources/application-key.yaml @@ -18,4 +18,7 @@ aws: iot-access-key: ${AWS_IOT_ACCESS_KEY} iot-secret-key: ${AWS_IOT_SECRET_KEY} iot-endpoint: ${AWS_IOT_ENDPOINT} - iot-client-id: ${AWS_IOT_CLIENT_ID} \ No newline at end of file + iot-client-id: ${AWS_IOT_CLIENT_ID} + s3-access-key: ${AWS_S3_ACCESS_KEY} + s3-secret-key: ${AWS_S3_SECRET_KEY} + s3-bucket: ${AWS_S3_BUCKET} \ No newline at end of file