diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 1830036c..f9e61838 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -57,6 +57,10 @@ jobs: SPRING_DATASOURCE_PASSWORD: testpassword CORS_ALLOWED_ORIGINS: ${{ secrets.CORS_ALLOWED_ORIGINS }} CCTV_STREAM_URL_PREFIX: ${{ secrets.CCTV_STREAM_URL_PREFIX }} + CLOUD_AWS_BUCKET: ${{secrets.CLOUD_AWS_BUCKET}} + CLOUD_AWS_REGION_STATIC: ${{secrets.CLOUD_AWS_REGION_STATIC}} + CLOUD_AWS_CREDENTIALS_ACCESS_KEY: ${{secrets.CLOUD_AWS_CREDENTIALS_ACCESS_KEY}} + CLOUD_AWS_CREDENTIALS_SECRET_KEY: ${{secrets.CLOUD_AWS_CREDENTIALS_SECRET_KEY}} run: ./gradlew test dependency-submission: @@ -79,4 +83,5 @@ jobs: uses: gradle/actions/dependency-submission@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 with: gradle-version: '8.12.1' - build-root-directory: ./backend \ No newline at end of file + build-root-directory: ./backend + diff --git a/backend/build.gradle b/backend/build.gradle index ddf3a96b..be065994 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -35,6 +35,12 @@ dependencies { // Lombok 의존성 추가 compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' + + // S3 + implementation('com.amazonaws:aws-java-sdk-s3:1.12.543') { + // 기존 취약한 ion-java 제외하고 + exclude group: 'software.amazon.ion', module: 'ion-java' + } } tasks.named('test') { diff --git a/backend/src/main/java/com/example/backend/config/S3Config.java b/backend/src/main/java/com/example/backend/config/S3Config.java new file mode 100644 index 00000000..98900935 --- /dev/null +++ b/backend/src/main/java/com/example/backend/config/S3Config.java @@ -0,0 +1,32 @@ +package com.example.backend.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; + + +@Configuration +public class S3Config { + + @Bean + public AWSCredentialsProvider awsCredentialsProvider( + @Value("${cloud.aws.credentials.access-key}") String accessKey, + @Value("${cloud.aws.credentials.secret-key}") String secretKey) { + BasicAWSCredentials creds = new BasicAWSCredentials(accessKey, secretKey); + return new AWSStaticCredentialsProvider(creds); + } + + @Bean + public AmazonS3 amazonS3(AWSCredentialsProvider awsCredentialsProvider, + @Value("${cloud.aws.region.static}") String region) { + return AmazonS3ClientBuilder.standard() + .withCredentials(awsCredentialsProvider) + .withRegion(region) + .build(); + } +} diff --git a/backend/src/main/java/com/example/backend/dashboard/service/DashboardService.java b/backend/src/main/java/com/example/backend/dashboard/service/DashboardService.java index d8c254cf..520027e9 100644 --- a/backend/src/main/java/com/example/backend/dashboard/service/DashboardService.java +++ b/backend/src/main/java/com/example/backend/dashboard/service/DashboardService.java @@ -1,5 +1,7 @@ package com.example.backend.dashboard.service; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.example.backend.common.domain.CaseEntity; import com.example.backend.common.domain.PoliceEntity; import com.example.backend.dashboard.dto.DashboardResponse; @@ -10,19 +12,28 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; + +import com.amazonaws.services.s3.AmazonS3; + + @Service @Transactional @RequiredArgsConstructor public class DashboardService { + @Value("${cloud.aws.bucket}") + private String bucket; + private final AmazonS3 s3Client; private final DashboardRepository dashboardRepository; // 세션에서 officeId 추출 @@ -80,12 +91,12 @@ public List getCases(HttpSession session) { .collect(Collectors.toList()); } - // id별 사건 영상 확인 + // id별 사건 영상 조회 public Map getCaseVideo(int id, HttpSession session) { CaseEntity caseEntity = getAuthorizedCase(id, session); - String videoUrl = caseEntity.getVideo(); - if (videoUrl == null || videoUrl.trim().isEmpty()) { + String videoKey = caseEntity.getVideo(); + if (videoKey == null || videoKey.trim().isEmpty()) { throw new EntityNotFoundException("해당 사건에 대한 영상이 없습니다."); } @@ -94,7 +105,20 @@ public Map getCaseVideo(int id, HttpSession session) { dashboardRepository.save(caseEntity); } - return Collections.singletonMap("video", videoUrl); + // Presigned URL 유효기간 설정 (30분) + Date expiration = new Date(); + long expTime = expiration.getTime(); + expTime += TimeUnit.MINUTES.toMillis(30); // 30 minute + expiration.setTime(expTime); + + GeneratePresignedUrlRequest presignRequest = + new GeneratePresignedUrlRequest(bucket, videoKey) + .withMethod(HttpMethod.GET) + .withExpiration(expiration); + + String url = s3Client.generatePresignedUrl(presignRequest).toString(); + return Collections.singletonMap("video", url); + } // 출동, 미출동 상태 변경 diff --git a/backend/src/main/java/com/example/backend/search/dto/DetailResponse.java b/backend/src/main/java/com/example/backend/search/dto/DetailResponse.java index 0b0d9375..e17b98ef 100644 --- a/backend/src/main/java/com/example/backend/search/dto/DetailResponse.java +++ b/backend/src/main/java/com/example/backend/search/dto/DetailResponse.java @@ -39,4 +39,11 @@ public static DetailResponse fromEntity(CaseEntity entity) { .memo(entity.getMemo()) .build(); } + + // 오버로드: presignedUrl을 video 필드에 덮어씌워 반환 + public static DetailResponse fromEntity(CaseEntity e, String presignedUrl) { + DetailResponse r = fromEntity(e); + r.setVideo(presignedUrl); + return r; + } } diff --git a/backend/src/main/java/com/example/backend/search/service/SearchService.java b/backend/src/main/java/com/example/backend/search/service/SearchService.java index 554002c2..2491de5c 100644 --- a/backend/src/main/java/com/example/backend/search/service/SearchService.java +++ b/backend/src/main/java/com/example/backend/search/service/SearchService.java @@ -1,17 +1,23 @@ package com.example.backend.search.service; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.example.backend.common.domain.CaseEntity; import com.example.backend.search.domain.SearchSpecification; import com.example.backend.search.dto.*; import com.example.backend.search.repository.SearchRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Service @@ -58,11 +64,29 @@ public SearchResult getCheckLog(String category, LocalDateTime startDate, LocalD .build(); } + @Value("${cloud.aws.bucket}") + private String bucket; + private final AmazonS3 s3Client; public DetailResponse getLogById(Integer id) { CaseEntity caseEntity = searchRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("해당 ID의 로그를 찾을 수 없습니다.")); - return DetailResponse.fromEntity(caseEntity); + String videoKey = caseEntity.getVideo(); + + // Presigned URL 유효기간 설정 (30분) + Date expiration = new Date(); + long expTime = expiration.getTime(); + expTime += TimeUnit.MINUTES.toMillis(30); // 30 minute + expiration.setTime(expTime); + + GeneratePresignedUrlRequest presignRequest = + new GeneratePresignedUrlRequest(bucket, videoKey) + .withMethod(HttpMethod.GET) + .withExpiration(expiration); + + String presignedUrl = s3Client.generatePresignedUrl(presignRequest).toString(); + + return DetailResponse.fromEntity(caseEntity, presignedUrl); }