Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -79,4 +83,5 @@ jobs:
uses: gradle/actions/dependency-submission@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
with:
gradle-version: '8.12.1'
build-root-directory: ./backend
build-root-directory: ./backend

6 changes: 6 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
32 changes: 32 additions & 0 deletions backend/src/main/java/com/example/backend/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 추출
Expand Down Expand Up @@ -80,12 +91,12 @@ public List<DashboardResponse> getCases(HttpSession session) {
.collect(Collectors.toList());
}

// id별 사건 영상 확인
// id별 사건 영상 조회
public Map<String, String> 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("해당 사건에 대한 영상이 없습니다.");
}

Expand All @@ -94,7 +105,20 @@ public Map<String, String> 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);

}

// 출동, 미출동 상태 변경
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}


Expand Down
Loading