Skip to content

Commit d176d25

Browse files
committed
hotfix : 상엽이 코드 추가
1 parent 3a3f3a4 commit d176d25

7 files changed

Lines changed: 121 additions & 15 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
echo "spring.servlet.multipart.file-size-threshold=2MB" >> src/main/resources/application.properties
3939
echo "server.tomcat.max-swallow-size=-1" >> src/main/resources/application.properties
4040
echo "server.tomcat.max-http-form-post-size=-1" >> src/main/resources/application.properties
41+
echo "fastapi.base-url=${{ secrets.FASTAPI_BASE_URL }}" >> src/main/resources/application.properties
4142
4243
- name: ☕ Gradle Build (jar 생성)
4344
run: ./gradlew build -x test

‎src/main/java/com/full/moon/config/WebClientConfig.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.full.moon.config;
22

33
import java.time.Duration;
4+
import java.util.concurrent.TimeUnit;
45

56
import org.springframework.beans.factory.annotation.Value;
67
import org.springframework.context.annotation.Bean;
78
import org.springframework.context.annotation.Configuration;
89
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.MediaType;
911
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
1012
import org.springframework.web.reactive.function.client.ExchangeStrategies;
1113
import org.springframework.web.reactive.function.client.WebClient;
@@ -47,4 +49,41 @@ public WebClient webClient() {
4749
// .exchangeStrategies(strategies)
4850
// .build();
4951
// }
52+
53+
// @Bean
54+
// public WebClient fastApiWebClient(
55+
// @Value("${fastapi.base-url}") String baseUrl
56+
// ) {
57+
// HttpClient httpClient = HttpClient.create()
58+
// .responseTimeout(java.time.Duration.ofSeconds(300));
59+
//
60+
// return WebClient.builder()
61+
// .baseUrl(baseUrl) // 예: http://localhost:8000 또는 http://fastapi:8000 (docker)
62+
// .clientConnector(new ReactorClientHttpConnector(httpClient))
63+
// .codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(100 * 1024 * 1024)) // 2MB
64+
// .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
65+
// .build();
66+
// }
67+
68+
69+
@Bean
70+
public WebClient fastApiWebClient(@Value("${fastapi.base-url}") String baseUrl) {
71+
HttpClient httpClient = HttpClient.create()
72+
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000)
73+
// 서버가 5분 걸리므로 넉넉히 6분
74+
.responseTimeout(Duration.ofMinutes(6))
75+
.doOnConnected(conn -> {
76+
conn.addHandlerLast(new ReadTimeoutHandler(6, TimeUnit.MINUTES));
77+
conn.addHandlerLast(new WriteTimeoutHandler(6, TimeUnit.MINUTES));
78+
});
79+
80+
return WebClient.builder()
81+
.baseUrl(baseUrl) // 예: http://localhost:8000 또는 docker 내부면 http://fastapi:8000
82+
.clientConnector(new ReactorClientHttpConnector(httpClient))
83+
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
84+
// 큰 응답 대비(필요 시)
85+
.codecs(c -> c.defaultCodecs().maxInMemorySize(4 * 1024 * 1024)) // 4MB
86+
.build();
87+
}
88+
5089
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.full.moon.domain.bookpage.dto;
2+
3+
public record ImageProcessRequest(String s3_image_url) {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.full.moon.domain.bookpage.dto;
2+
3+
public record ImageProcessResponse(String url) {
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.full.moon.domain.bookpage.dto;
2+
3+
import java.util.List;
4+
5+
public record VideoResponse(List<String> video_urls) {
6+
}

‎src/main/java/com/full/moon/domain/bookpage/service/BookPageService.java‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package com.full.moon.domain.bookpage.service;
22

3+
import java.io.IOException;
4+
import java.time.Duration;
5+
36
import org.springframework.data.domain.Page;
47
import org.springframework.data.domain.Pageable;
8+
import org.springframework.http.HttpStatusCode;
9+
import org.springframework.http.MediaType;
510
import org.springframework.stereotype.Service;
611
import org.springframework.web.multipart.MultipartFile;
12+
import org.springframework.web.reactive.function.client.WebClient;
713

814
import com.full.moon.domain.book.entitiy.Book;
915
import com.full.moon.domain.book.repository.BookRepository;
1016
import com.full.moon.domain.bookpage.dto.BookPageResponse;
17+
import com.full.moon.domain.bookpage.dto.ImageProcessRequest;
18+
import com.full.moon.domain.bookpage.dto.ImageProcessResponse;
19+
import com.full.moon.domain.bookpage.dto.VideoResponse;
1120
import com.full.moon.domain.bookpage.entitiy.BookPage;
1221
import com.full.moon.domain.bookpage.repository.BookPageRepository;
1322
import com.full.moon.domain.child.entity.Child;
@@ -20,6 +29,8 @@
2029
import com.full.moon.infra.s3.service.S3Service;
2130

2231
import lombok.RequiredArgsConstructor;
32+
import reactor.core.publisher.Mono;
33+
import reactor.util.retry.Retry;
2334

2435
@Service
2536
@RequiredArgsConstructor
@@ -30,6 +41,7 @@ public class BookPageService {
3041
private final BookRepository bookRepository;
3142
private final S3Service s3Service;
3243
private final UserUtils userUtils;
44+
private final WebClient fastApiWebClient;
3345

3446
public BookPageResponse makeNewBookPage(CustomOAuth2User customOAuth2User,Long childId, Long bookId, MultipartFile file){
3547

@@ -45,11 +57,12 @@ public BookPageResponse makeNewBookPage(CustomOAuth2User customOAuth2User,Long c
4557
String imgUrl = s3Service.uploadFile(file);
4658
//여기서 FastAPI server로 요청보냄
4759

60+
String videoUrl = processBlocking(imgUrl);
4861

4962
BookPage bookPage = BookPage.builder()
5063
.book(book)
5164
.originalImageUrl(imgUrl)
52-
.videoUrl("")
65+
.videoUrl(videoUrl)
5366
.build();
5467

5568
bookPageRepository.save(bookPage);
@@ -77,4 +90,44 @@ public Page<BookPageResponse> getAllPage(Long bookId, Pageable pageable){
7790
return bookPageRepository.findAllByBook(book,pageable)
7891
.map(bookPage -> new BookPageResponse(bookPage.getId(),bookPage.getOriginalImageUrl(),bookPage.getVideoUrl()));
7992
}
93+
94+
private String processImageToVideos(String s3ImageUrl) {
95+
ImageProcessRequest request = new ImageProcessRequest(s3ImageUrl);
96+
97+
VideoResponse response = fastApiWebClient.post()
98+
.uri("/process-image-to-videos")
99+
.contentType(MediaType.APPLICATION_JSON)
100+
.accept(MediaType.APPLICATION_JSON)
101+
.bodyValue(request)
102+
.retrieve()
103+
.bodyToMono(VideoResponse.class)
104+
.block(); // 동기 처리
105+
106+
if (response == null || response.video_urls() == null || response.video_urls().isEmpty()) {
107+
throw new RuntimeException("FastAPI 응답에서 video_urls를 찾을 수 없습니다.");
108+
}
109+
110+
// 첫 번째 URL만 반환
111+
return response.video_urls().get(0);
112+
}
113+
114+
private String processBlocking(String s3ImageUrl) {
115+
ImageProcessRequest request = new ImageProcessRequest(s3ImageUrl);
116+
117+
VideoResponse res = fastApiWebClient.post()
118+
.uri("/process-image-to-videos")
119+
.contentType(MediaType.APPLICATION_JSON)
120+
.accept(MediaType.APPLICATION_JSON)
121+
.bodyValue(request)
122+
.retrieve()
123+
.bodyToMono(VideoResponse.class)
124+
.timeout(Duration.ofMinutes(6)) // 이 레벨에서도 가드
125+
.block();
126+
127+
if (res == null || res.video_urls() == null || res.video_urls().isEmpty()) {
128+
throw new IllegalStateException("FastAPI 응답에 video_urls가 없습니다.");
129+
}
130+
return res.video_urls().get(0); // 첫 번째만 사용
131+
}
132+
80133
}

‎src/main/java/com/full/moon/infra/s3/service/S3Service.java‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,31 @@ public class S3Service {
3737
private String secretKey;
3838

3939
public String uploadFile(MultipartFile file) {
40-
4140
S3Client s3 = S3Client.builder()
4241
.region(Region.of(region))
4342
.credentialsProvider(StaticCredentialsProvider.create(
44-
AwsBasicCredentials.create(accessKey, secretKey)
45-
))
43+
AwsBasicCredentials.create(accessKey, secretKey)))
4644
.build();
4745

4846
try {
49-
String fileName = "uploads/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
47+
String rawName = file.getOriginalFilename(); // 원본 파일명
48+
String safeName = URLEncoder.encode(rawName, StandardCharsets.UTF_8)
49+
.replace("+", "%20"); // 공백을 %20로
50+
String key = "test_images/" + UUID.randomUUID() + "_" + safeName;
5051

51-
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
52+
PutObjectRequest put = PutObjectRequest.builder()
5253
.bucket(bucketName)
53-
.key(fileName)
54+
.key(key)
5455
.contentType(file.getContentType())
56+
// 공개 URL로 바로 접근하려면 주석 해제
57+
// .acl(ObjectCannedACL.PUBLIC_READ)
5558
.build();
5659

57-
PutObjectResponse response = s3.putObject(
58-
putObjectRequest,
59-
software.amazon.awssdk.core.sync.RequestBody.fromInputStream(file.getInputStream(), file.getSize())
60-
);
61-
62-
log.info("S3 Upload Response: {}", response);
63-
String fileUrl = "https://" + bucketName + ".s3." + region + ".amazonaws.com/" + URLEncoder.encode(fileName,
64-
StandardCharsets.UTF_8);
60+
s3.putObject(put, software.amazon.awssdk.core.sync.RequestBody
61+
.fromInputStream(file.getInputStream(), file.getSize()));
6562

63+
// ⚠️ 여기서는 절대 다시 인코딩하지 말 것
64+
String fileUrl = "https://" + bucketName + ".s3." + region + ".amazonaws.com/" + key;
6665
return fileUrl;
6766

6867
} catch (S3Exception | IOException e) {

0 commit comments

Comments
 (0)