Skip to content

Commit b673270

Browse files
[Refactor] 배포환경 ì�S3연결 및 이미지생성기능í활성화 (#142)
1 parent 119aadf commit b673270

File tree

5 files changed

+59
-100
lines changed

5 files changed

+59
-100
lines changed

back/src/main/java/com/back/domain/scenario/entity/Scenario.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public class Scenario extends BaseEntity {
7878
@Column(columnDefinition = "TEXT")
7979
private String timelineTitles; // {"2020": "대학원 진학", "2022": "연구실 변경", "2025": "해외 학회"} 형태
8080

81-
// 시나리오 대표 이미지 URL
81+
// 시나리오 이미지 파일명
8282
private String img;
8383

8484
// 대표 시나리오 여부

back/src/main/java/com/back/global/ai/config/ImageAiConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public class ImageAiConfig {
4040
// AWS S3 리전 (storageType이 s3인 경우)
4141
private String s3Region;
4242

43+
// CloudFront 도메인 (storageType이 s3인 경우)
44+
private String cloudFrontDomain;
45+
4346
// 로컬 파일 저장 경로 (storageType="local"인 경우 사용)
4447
// 기본값: "./uploads/images"
4548
private String localStoragePath = "./uploads/images";

back/src/main/java/com/back/global/storage/S3StorageService.java

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*
2424
* CompletableFuture.supplyAsync() 사용하여 메모리 효율적 처리
2525
* S3Client는 기본 Connection Pool 사용 (리소스 최소화)
26+
* 파일명 저장
2627
* 파일명: UUID 기반으로 충돌 방지
2728
*/
2829
@Slf4j
@@ -58,15 +59,8 @@ public CompletableFuture<String> uploadBase64Image(String base64Data) {
5859
// S3 업로드
5960
s3Client.putObject(putRequest, RequestBody.fromBytes(imageBytes));
6061

61-
String s3Url = String.format(
62-
"https://%s.s3.%s.amazonaws.com/%s",
63-
imageAiConfig.getS3BucketName(),
64-
imageAiConfig.getS3Region(),
65-
fileName
66-
);
67-
68-
log.info("Image uploaded to S3 successfully: {}", s3Url);
69-
return s3Url;
62+
log.info("Image uploaded to S3 successfully with key: {}", fileName);
63+
return fileName;
7064

7165
} catch (IllegalArgumentException e) {
7266
log.error("Invalid Base64 data: {}", e.getMessage());
@@ -82,26 +76,33 @@ public CompletableFuture<String> uploadBase64Image(String base64Data) {
8276
}
8377

8478
@Override
85-
public CompletableFuture<Void> deleteImage(String imageUrl) {
79+
public CompletableFuture<Void> deleteImage(String imageUrl) { // imageUrl is now the filename
8680
return CompletableFuture.runAsync(() -> {
8781
try {
88-
String fileName = extractFileNameFromUrl(imageUrl);
82+
if (imageUrl == null || imageUrl.isEmpty()) {
83+
throw new ApiException(ErrorCode.STORAGE_INVALID_FILE, "Image key cannot be null or empty for deletion.");
84+
}
8985

9086
// S3 삭제 요청
9187
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
9288
.bucket(imageAiConfig.getS3BucketName())
93-
.key(fileName)
89+
.key(imageUrl)
9490
.build();
9591

9692
s3Client.deleteObject(deleteRequest);
97-
log.info("Image deleted from S3: {}", fileName);
93+
log.info("Image deleted from S3: {}", String.valueOf(imageUrl));
9894

99-
} catch (S3Exception e) {
100-
log.error("S3 service error during deletion: {}", e.getMessage(), e);
101-
throw new ApiException(ErrorCode.S3_CONNECTION_FAILED, "S3 delete failed: " + e.awsErrorDetails().errorMessage());
95+
} catch (ApiException e) {
96+
throw e;
10297
} catch (Exception e) {
103-
log.error("Failed to delete image from S3: {}", e.getMessage(), e);
104-
throw new ApiException(ErrorCode.STORAGE_DELETE_FAILED, "Failed to delete image from S3: " + e.getMessage());
98+
if (e instanceof S3Exception) {
99+
S3Exception s3e = (S3Exception) e;
100+
log.error("S3 service error during deletion: {}", s3e.getMessage(), s3e);
101+
throw new ApiException(ErrorCode.S3_CONNECTION_FAILED, "S3 delete failed: " + s3e.getMessage());
102+
} else {
103+
log.error("Failed to delete image from S3: {}", e.getMessage(), e);
104+
throw new ApiException(ErrorCode.STORAGE_DELETE_FAILED, "Failed to delete image from S3: " + e.getMessage());
105+
}
105106
}
106107
});
107108
}
@@ -111,13 +112,5 @@ public String getStorageType() {
111112
return "s3";
112113
}
113114

114-
//S3 URL에서 파일명 추출, 예:https://bucket.s3.region.amazonaws.com/scenario-uuid.jpeg → scenario-uuid.jpeg
115-
private String extractFileNameFromUrl(String imageUrl) {
116-
if (imageUrl == null || imageUrl.isEmpty()) {
117-
throw new ApiException(ErrorCode.STORAGE_INVALID_FILE, "Image URL cannot be null or empty");
118-
}
119115

120-
String[] parts = imageUrl.split("/");
121-
return parts[parts.length - 1];
122-
}
123116
}

back/src/main/resources/application-prod.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ custom:
7777
# AI Services Configuration (Production)
7878
ai:
7979
image:
80-
enabled: false # 프로덕션 환경에서는 활성화
80+
enabled: true # 프로덕션 환경에서는 활성화
8181
storage-type: s3 # S3 스토리지 사용
8282
s3-bucket-name: ${AWS_S3_BUCKET_NAME}
83-
s3-region: ${AWS_REGION}
83+
s3-region: ${AWS_REGION}
84+
cloud-front-domain: ${AWS_CLOUD_FRONT_DOMAIN} # 향후 확장성 고려, 현재는 미사용

back/src/test/java/com/back/global/storage/S3StorageServiceTest.java

Lines changed: 33 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ class UploadBase64ImageTests {
7979

8080
// Then
8181
assertThat(resultUrl).isNotNull();
82-
assertThat(resultUrl).startsWith("https://" + TEST_BUCKET_NAME + ".s3." + TEST_REGION + ".amazonaws.com/");
83-
assertThat(resultUrl).contains("scenario-");
82+
assertThat(resultUrl).startsWith("scenario-");
8483
assertThat(resultUrl).endsWith(".jpeg");
8584

8685
// S3Client 호출 검증
@@ -171,7 +170,7 @@ class UploadBase64ImageTests {
171170
String resultUrl = s3StorageService.uploadBase64Image(base64Data).get();
172171

173172
// Then - S3 표준 URL 형식: https://{bucket}.s3.{region}.amazonaws.com/{key}
174-
assertThat(resultUrl).matches("https://test-bucket\\.s3\\.ap-northeast-2\\.amazonaws\\.com/scenario-[a-f0-9\\-]+\\.jpeg");
173+
assertThat(resultUrl).matches("scenario-[a-f0-9\\-]+\\.jpeg");
175174
}
176175
}
177176

@@ -183,46 +182,49 @@ class DeleteImageTests {
183182
@DisplayName("성공 - S3 이미지 삭제")
184183
void deleteImage_성공_S3_이미지_삭제() throws ExecutionException, InterruptedException {
185184
// Given
186-
String s3Url = "https://test-bucket.s3.ap-northeast-2.amazonaws.com/scenario-test-uuid.jpeg";
185+
String key = "scenario-test-uuid.jpeg";
187186
DeleteObjectResponse mockResponse = DeleteObjectResponse.builder().build();
188187
given(s3Client.deleteObject(any(DeleteObjectRequest.class)))
189188
.willReturn(mockResponse);
190189

191190
// When
192-
CompletableFuture<Void> deleteFuture = s3StorageService.deleteImage(s3Url);
191+
CompletableFuture<Void> deleteFuture = s3StorageService.deleteImage(key);
193192
deleteFuture.get();
194193

195194
// Then
196-
verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class));
195+
verify(s3Client, times(1)).deleteObject(argThat((DeleteObjectRequest request) ->
196+
request.key().equals(key)
197+
));
197198
}
198199

199200
@Test
200201
@DisplayName("실패 - S3 서비스 에러")
201202
void deleteImage_실패_S3_서비스_에러() {
202203
// Given
203-
String s3Url = "https://test-bucket.s3.ap-northeast-2.amazonaws.com/scenario-test-uuid.jpeg";
204-
205-
AwsErrorDetails errorDetails = AwsErrorDetails.builder()
206-
.errorMessage("NoSuchKey")
207-
.build();
208-
S3Exception s3Exception = (S3Exception) S3Exception.builder()
209-
.awsErrorDetails(errorDetails)
210-
.message("S3 Error")
211-
.build();
212-
213-
doThrow(s3Exception).when(s3Client).deleteObject(any(DeleteObjectRequest.class));
214-
215-
// When
216-
CompletableFuture<Void> deleteFuture = s3StorageService.deleteImage(s3Url);
217-
218-
// Then
219-
assertThatThrownBy(deleteFuture::get)
220-
.hasCauseInstanceOf(ApiException.class)
221-
.cause()
222-
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.S3_CONNECTION_FAILED);
223-
224-
verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class));
225-
}
204+
String key = "scenario-test-uuid.jpeg";
205+
206+
AwsErrorDetails errorDetails = AwsErrorDetails.builder()
207+
.errorMessage("NoSuchKey")
208+
.build();
209+
S3Exception s3Exception = (S3Exception) S3Exception.builder()
210+
.awsErrorDetails(errorDetails)
211+
.message("S3 Error")
212+
.build();
213+
214+
doThrow(s3Exception).when(s3Client).deleteObject(any(DeleteObjectRequest.class));
215+
216+
// When
217+
CompletableFuture<Void> deleteFuture = s3StorageService.deleteImage(key);
218+
219+
// Then
220+
assertThatThrownBy(deleteFuture::get)
221+
.hasCauseInstanceOf(ApiException.class)
222+
.cause()
223+
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.S3_CONNECTION_FAILED);
224+
225+
verify(s3Client, times(1)).deleteObject(argThat((DeleteObjectRequest request) ->
226+
request.key().equals(key)
227+
)); }
226228

227229
@Test
228230
@DisplayName("실패 - null URL")
@@ -237,7 +239,7 @@ class DeleteImageTests {
237239
assertThatThrownBy(deleteFuture::get)
238240
.hasCauseInstanceOf(ApiException.class)
239241
.cause()
240-
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.STORAGE_DELETE_FAILED);
242+
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.STORAGE_INVALID_FILE);
241243

242244
// S3Client는 호출되지 않아야 함
243245
verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class));
@@ -256,53 +258,13 @@ class DeleteImageTests {
256258
assertThatThrownBy(deleteFuture::get)
257259
.hasCauseInstanceOf(ApiException.class)
258260
.cause()
259-
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.STORAGE_DELETE_FAILED);
261+
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.STORAGE_INVALID_FILE);
260262

261263
// S3Client는 호출되지 않아야 함
262264
verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class));
263265
}
264266
}
265267

266-
@Nested
267-
@DisplayName("URL 파싱")
268-
class ExtractFileNameTests {
269-
270-
@Test
271-
@DisplayName("성공 - S3 URL에서 파일명 추출")
272-
void extractFileName_성공_S3_URL에서_파일명_추출() throws ExecutionException, InterruptedException {
273-
// Given
274-
String s3Url = "https://test-bucket.s3.ap-northeast-2.amazonaws.com/scenario-abc-123.jpeg";
275-
DeleteObjectResponse mockResponse = DeleteObjectResponse.builder().build();
276-
given(s3Client.deleteObject(any(DeleteObjectRequest.class)))
277-
.willReturn(mockResponse);
278-
279-
// When - deleteImage 내부에서 extractFileNameFromUrl 호출됨
280-
s3StorageService.deleteImage(s3Url).get();
281-
282-
// Then - 정상적으로 파일명 추출 및 삭제 요청 성공
283-
verify(s3Client, times(1)).deleteObject(argThat((DeleteObjectRequest request) ->
284-
request.key().equals("scenario-abc-123.jpeg")
285-
));
286-
}
287-
288-
@Test
289-
@DisplayName("성공 - 복잡한 S3 URL 파싱")
290-
void extractFileName_성공_복잡한_S3_URL_파싱() throws ExecutionException, InterruptedException {
291-
// Given - 경로가 있는 복잡한 URL
292-
String complexUrl = "https://test-bucket.s3.ap-northeast-2.amazonaws.com/images/2024/scenario-test.jpeg";
293-
DeleteObjectResponse mockResponse = DeleteObjectResponse.builder().build();
294-
given(s3Client.deleteObject(any(DeleteObjectRequest.class)))
295-
.willReturn(mockResponse);
296-
297-
// When
298-
s3StorageService.deleteImage(complexUrl).get();
299-
300-
// Then - 마지막 부분만 추출
301-
verify(s3Client, times(1)).deleteObject(argThat((DeleteObjectRequest request) ->
302-
request.key().equals("scenario-test.jpeg")
303-
));
304-
}
305-
}
306268

307269
@Nested
308270
@DisplayName("스토리지 타입")

0 commit comments

Comments
 (0)