-
Notifications
You must be signed in to change notification settings - Fork 0
feat: s3 파일 업로드 구현 및 aws lambda를 활용한 이미지 프로세싱 구조로 변경 #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6d75821
Feat: WebClient 사용하기 위한 의존성 추가 (WebFlux)
Bumnote ada489a
feat: S3 이미지 업로드 로직 기능 구현
Bumnote 9b4924c
remove: aws lambda 함수로 분리하여 사용하지 않는 메서드 제거
Bumnote eba2344
feat: 이미지 업로드 시, 프론트와 SSE 통신 로직 구현
Bumnote e44e939
feat: config 파일 작성
Bumnote File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 70 additions & 19 deletions
89
src/main/java/ssafy/retrip/api/controller/retrip/RetripController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,93 @@ | ||
| package ssafy.retrip.api.controller.retrip; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import java.io.IOException; | ||
| import java.util.List; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.http.codec.ServerSentEvent; | ||
| import org.springframework.http.codec.multipart.FilePart; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| 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.RequestParam; | ||
| import org.springframework.web.bind.annotation.RequestPart; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import ssafy.retrip.api.controller.retrip.response.TravelAnalysisResponseDto; | ||
| import reactor.core.publisher.Flux; | ||
| import reactor.core.publisher.Mono; | ||
| import ssafy.retrip.api.controller.retrip.request.LambdaCallbackRequest; | ||
| import ssafy.retrip.api.controller.retrip.request.LambdaFailureRequest; | ||
| import ssafy.retrip.api.controller.retrip.response.JobStatusResponse; | ||
| import ssafy.retrip.api.controller.retrip.response.JobUploadResponse; | ||
| import ssafy.retrip.api.service.retrip.RetripService; | ||
| import ssafy.retrip.api.service.sse.SseService; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/images") | ||
| @RequestMapping("/api") | ||
| public class RetripController { | ||
|
|
||
| private final SseService sseService; | ||
| private final RetripService retripService; | ||
|
|
||
| @PostMapping("/uploads") | ||
| public CompletableFuture<ResponseEntity<TravelAnalysisResponseDto>> uploadMultipleImages(HttpServletRequest request, | ||
| @RequestParam("images") List<MultipartFile> images) throws IOException { | ||
| @PostMapping("/images/uploads") | ||
| public Mono<ResponseEntity<JobUploadResponse>> uploadImages(@RequestPart(value = "images") Flux<FilePart> images) { | ||
|
|
||
| return retripService.createRetripFromImages(images) | ||
| .thenApply(result -> { | ||
| log.info("여행 분석 완료: retripId={}", result.getRetripId()); | ||
| return ResponseEntity.ok(result); | ||
| return retripService.uploadImagesToS3(images) | ||
| .map(jobId -> { | ||
| log.info("이미지 업로드 완료, jobId 반환: {}", jobId); | ||
| return ResponseEntity.ok(JobUploadResponse.of(jobId)); | ||
| }) | ||
| .exceptionally(ex -> { | ||
| log.error("여행 분석 처리 중 오류 발생", ex); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body(null); | ||
| .onErrorResume(ex -> { | ||
| log.error("이미지 업로드 중 오류 발생", ex); | ||
| return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null)); | ||
| }); | ||
| } | ||
|
|
||
| @GetMapping(value = "/retrips/{jobId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
| public Flux<ServerSentEvent<String>> streamResult(@PathVariable String jobId) { | ||
| log.info("SSE 연결 요청: jobId={}", jobId); | ||
| return sseService.connect(jobId); | ||
| } | ||
|
|
||
| @PostMapping("/internal/retrips/{jobId}/complete") | ||
| public Mono<ResponseEntity<Void>> handleLambdaCallback( | ||
| @PathVariable String jobId, | ||
| @RequestBody LambdaCallbackRequest request | ||
| ) { | ||
|
|
||
| log.info("Lambda 콜백 수신: jobId={}", jobId); | ||
|
|
||
| return retripService.handleAnalysisResult(jobId, request) | ||
| .then(Mono.just(ResponseEntity.ok().<Void>build())) | ||
| .onErrorResume(ex -> { | ||
| log.error("Lambda 콜백 처리 실패: jobId={}", jobId, ex); | ||
| return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); | ||
| }); | ||
| } | ||
|
|
||
| @PostMapping("/internal/retrips/{jobId}/fail") | ||
| public Mono<ResponseEntity<Void>> handleLambdaFailure( | ||
| @PathVariable String jobId, | ||
| @RequestBody LambdaFailureRequest request | ||
| ) { | ||
|
|
||
| log.error("Lambda 실패 콜백 수신: jobId={}, error={}", jobId, request.getErrorMessage()); | ||
|
|
||
| return retripService.handleFailure(jobId, request.getErrorMessage()) | ||
| .then(Mono.just(ResponseEntity.ok().<Void>build())) | ||
| .onErrorResume(ex -> { | ||
| log.error("Lambda 실패 콜백 처리 중 오류: jobId={}", jobId, ex); | ||
| return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); | ||
| }); | ||
| } | ||
|
|
||
| @GetMapping("/retrips/{jobId}/status") | ||
| public Mono<ResponseEntity<JobStatusResponse>> getJobStatus(@PathVariable String jobId) { | ||
| return retripService.getJobStatus(jobId) | ||
| .map(ResponseEntity::ok) | ||
| .defaultIfEmpty(ResponseEntity.notFound().build()); | ||
| } | ||
| } | ||
46 changes: 46 additions & 0 deletions
46
src/main/java/ssafy/retrip/api/controller/retrip/request/LambdaCallbackRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package ssafy.retrip.api.controller.retrip.request; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import ssafy.retrip.api.service.openai.response.AnalysisResponse; | ||
| import ssafy.retrip.api.service.retrip.info.ImageMetaData; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class LambdaCallbackRequest { | ||
|
|
||
| private AnalysisResponse analysisResponse; | ||
| private List<MetadataDto> metadata; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public static class MetadataDto { | ||
|
|
||
| private LocalDateTime takenDate; | ||
| private Double latitude; | ||
| private Double longitude; | ||
|
|
||
| public ImageMetaData toImageMetaData() { | ||
| ImageMetaData meta = new ImageMetaData(); | ||
| meta.updateTakenDate(this.takenDate); | ||
| if (this.latitude != null && this.longitude != null) { | ||
| meta.updateGeoLocation(this.latitude, this.longitude); | ||
| } | ||
| return meta; | ||
| } | ||
| } | ||
|
|
||
| public List<ImageMetaData> toImageMetaDataList() { | ||
| if (metadata == null) { | ||
| return List.of(); | ||
| } | ||
| return metadata.stream() | ||
| .map(MetadataDto::toImageMetaData) | ||
| .toList(); | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
src/main/java/ssafy/retrip/api/controller/retrip/request/LambdaFailureRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package ssafy.retrip.api.controller.retrip.request; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class LambdaFailureRequest { | ||
|
|
||
| private String errorMessage; | ||
| } |
47 changes: 47 additions & 0 deletions
47
src/main/java/ssafy/retrip/api/controller/retrip/response/JobStatusResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package ssafy.retrip.api.controller.retrip.response; | ||
|
|
||
|
|
||
| import static ssafy.retrip.domain.job.JobStatus.COMPLETED; | ||
| import static ssafy.retrip.domain.job.JobStatus.FAILED; | ||
| import static ssafy.retrip.domain.job.JobStatus.PROCESSING; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import ssafy.retrip.domain.job.JobStatus; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class JobStatusResponse { | ||
|
|
||
| private String jobId; | ||
| private JobStatus status; | ||
| private TravelAnalysisResponseDto result; | ||
| private String errorMessage; | ||
|
|
||
| public static JobStatusResponse processing(String jobId) { | ||
| return JobStatusResponse.builder() | ||
| .jobId(jobId) | ||
| .status(PROCESSING) | ||
| .build(); | ||
| } | ||
|
|
||
| public static JobStatusResponse completed(String jobId, TravelAnalysisResponseDto result) { | ||
| return JobStatusResponse.builder() | ||
| .jobId(jobId) | ||
| .status(COMPLETED) | ||
| .result(result) | ||
| .build(); | ||
| } | ||
|
|
||
| public static JobStatusResponse failed(String jobId, String errorMessage) { | ||
| return JobStatusResponse.builder() | ||
| .jobId(jobId) | ||
| .status(FAILED) | ||
| .errorMessage(errorMessage) | ||
| .build(); | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
src/main/java/ssafy/retrip/api/controller/retrip/response/JobUploadResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package ssafy.retrip.api.controller.retrip.response; | ||
|
|
||
| import static ssafy.retrip.domain.job.JobStatus.PROCESSING; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import ssafy.retrip.domain.job.JobStatus; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public class JobUploadResponse { | ||
|
|
||
| private String jobId; | ||
| private JobStatus status; | ||
| private String message; | ||
|
|
||
| public static JobUploadResponse of(String jobId) { | ||
| return new JobUploadResponse(jobId, PROCESSING, "이미지 업로드 완료. SSE 또는 폴링으로 결과를 확인하세요."); | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
src/main/java/ssafy/retrip/api/service/cache/RetripResultCacheService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package ssafy.retrip.api.service.cache; | ||
|
|
||
| import java.time.Duration; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
| import reactor.core.publisher.Mono; | ||
| import reactor.core.scheduler.Schedulers; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class RetripResultCacheService { | ||
|
|
||
| private static final String KEY_PREFIX = "retrip:result:"; | ||
| private static final Duration CACHE_TTL = Duration.ofMinutes(10); | ||
|
|
||
| private final RedisTemplate<String, String> redisTemplate; | ||
|
|
||
| public Mono<Void> cacheResult(String jobId, String resultJson) { | ||
| return Mono.fromRunnable(() -> { | ||
| String key = KEY_PREFIX + jobId; | ||
| redisTemplate.opsForValue().set(key, resultJson, CACHE_TTL); | ||
| log.info("Redis 결과 캐싱 완료: jobId={}, TTL={}분", jobId, CACHE_TTL.toMinutes()); | ||
| }) | ||
| .subscribeOn(Schedulers.boundedElastic()) | ||
| .then(); | ||
| } | ||
|
|
||
| public Mono<String> getCachedResult(String jobId) { | ||
| return Mono.fromCallable(() -> { | ||
| String key = KEY_PREFIX + jobId; | ||
| return redisTemplate.opsForValue().get(key); | ||
| }) | ||
| .subscribeOn(Schedulers.boundedElastic()) | ||
| .flatMap(result -> result != null ? Mono.just(result) : Mono.empty()); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/internal/retrips/{jobId}/complete,/internal/retrips/{jobId}/fail가 외부에서 직접 호출 가능하며, 현재 Security 설정도 전체 permitAll이라 콜백 위조가 가능합니다. Lambda가 보낸 요청임을 검증할 수 있도록 (예: 공유 시크릿 헤더/X-Signature, IAM authorizer를 거친 API Gateway, IP allowlist 등) 최소 1개의 서버측 검증을 추가하세요.