Skip to content

feat: PreSigned URL Content-Type 지원 추가#571

Merged
Whale0928 merged 2 commits intomainfrom
feat/presign-url
Mar 24, 2026
Merged

feat: PreSigned URL Content-Type 지원 추가#571
Whale0928 merged 2 commits intomainfrom
feat/presign-url

Conversation

@Whale0928
Copy link
Copy Markdown
Collaborator

Summary

  • PreSigned URL 발급 시 contentType 파라미터를 입력받아 확장자와 Content-Type을 동적으로 처리
  • 허용 목록: image/jpeg, image/png, image/webp, video/mp4
  • 허용되지 않은 Content-Type 요청 시 400 예외 처리
  • product-api, admin-api RestDocs 문서에 contentType 파라미터 반영

Related: bottle-note/workspace#205

Test plan

  • contentType별 확장자 매핑 단위 테스트 (image/jpeg->jpg, image/png->png, image/webp->webp, video/mp4->mp4)
  • 허용되지 않은 contentType 예외 발생 테스트
  • 기존 테스트 전체 통과 확인 (9/9)
  • product-api RestDocs 테스트 통과
  • admin-api RestDocs 테스트 통과

🤖 Generated with Claude Code

- ImageUploadRequest에 contentType 파라미터 추가 (기본값 image/jpeg)
- PreSignUrlProvider에 MIME -> 확장자 허용 목록 적용 (jpg, png, webp, mp4)
- GeneratePresignedUrlRequest로 Content-Type 강제 적용
- 허용되지 않은 Content-Type 요청 시 UNSUPPORTED_CONTENT_TYPE 예외 처리
- contentType별 확장자 검증 단위 테스트 추가
- product-api, admin-api RestDocs에 contentType 파라미터 문서화

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 24, 2026 03:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

PreSigned URL 발급 시 contentType을 받아 S3 오브젝트 확장자 및 Presigned URL의 Content-Type을 동적으로 처리하고, 허용되지 않은 Content-Type은 400으로 차단하도록 변경합니다. 또한 product-api/admin-api RestDocs 및 관련 테스트를 업데이트합니다.

Changes:

  • contentType 기반 확장자 매핑(허용 목록) 및 미지원 Content-Type 예외(400) 추가
  • Presigned URL 생성 시 GeneratePresignedUrlRequest.withContentType(contentType) 적용
  • product/admin RestDocs 및 단위 테스트에서 contentType 파라미터 반영

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
plan/presigned-url-content-type.md Content-Type 기반 Presigned URL 설계/변경점 계획 문서 추가
plan/banner-media-type.md 배너의 미디어 타입 분리 작업 계획 문서 추가(관련 문서 링크)
git.environment-variables 서브모듈 커밋 포인터 업데이트
bottlenote-product-api/src/test/java/app/docs/upload/RestImageUploadControllerTest.java product-api RestDocs에 contentType 쿼리 파라미터 문서화
bottlenote-product-api/src/test/java/app/bottlenote/common/file/upload/fixture/FakeAmazonS3.java 테스트용 presigned URL 생성 로직을 S3 형태로 변경
bottlenote-product-api/src/test/java/app/bottlenote/common/file/upload/ImageUploadServiceTest.java contentType별 확장자 및 예외 케이스 단위 테스트 추가/갱신
bottlenote-mono/src/test/java/app/bottlenote/common/file/ImageUploadUnitTest.java mono 단위 테스트에서 신규 request 시그니처 반영
bottlenote-mono/src/main/java/app/bottlenote/common/file/service/ImageUploadService.java presigned URL 생성에 contentType 전달 및 request 빌더 사용
bottlenote-mono/src/main/java/app/bottlenote/common/file/exception/FileExceptionCode.java 미지원 Content-Type 예외 코드 추가
bottlenote-mono/src/main/java/app/bottlenote/common/file/dto/request/ImageUploadRequest.java contentType 필드 추가 및 기본값(image/jpeg) 적용
bottlenote-mono/src/main/java/app/bottlenote/common/file/PreSignUrlProvider.java 허용 Content-Type→확장자 매핑 및 키 생성 시 검증/적용
bottlenote-admin-api/src/test/kotlin/app/docs/file/AdminImageUploadControllerDocsTest.kt admin-api RestDocs에 contentType 쿼리 파라미터 문서화

Comment on lines +41 to +44
String bucketName = generatePresignedUrlRequest.getBucketName();
String key = generatePresignedUrlRequest.getKey();
url = new URL("https", bucketName + ".s3.amazonaws.com", "/" + key);
System.out.println("Fake url 생성 : " + url);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.out.println은 테스트 출력 노이즈를 만들고 CI 로그를 불필요하게 오염시킬 수 있습니다. 필요하다면 테스트 로거를 사용해 debug 레벨로 남기거나, 단순히 출력 라인을 제거하는 방식으로 정리하는 것이 좋습니다.

Copilot uses AI. Check for mistakes.
* @return 생성된 오브젝트 키
*/
default String getImageKey(String rootPath, Long index) {
default String getImageKey(String rootPath, Long index, String contentType) {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MIME media type은 규격상 대소문자/공백에 대해 관대하게 처리되는 경우가 많습니다(예: "Image/JPEG", "image/jpeg; charset=utf-8", 앞뒤 공백). 현재는 Map key와 완전 일치만 허용해서 유효한 입력도 400으로 거절될 수 있으니, lookup 전에 trim() + 소문자 정규화 및 필요 시 파라미터(예: ; charset=) 제거 또는 Spring MediaType 파싱 기반 비교로 허용 여부를 판단하도록 개선하는 것을 권장합니다.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
String extension = ALLOWED_CONTENT_TYPES.get(contentType);
if (extension == null) {
throw new FileException(UNSUPPORTED_CONTENT_TYPE);
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MIME media type은 규격상 대소문자/공백에 대해 관대하게 처리되는 경우가 많습니다(예: "Image/JPEG", "image/jpeg; charset=utf-8", 앞뒤 공백). 현재는 Map key와 완전 일치만 허용해서 유효한 입력도 400으로 거절될 수 있으니, lookup 전에 trim() + 소문자 정규화 및 필요 시 파라미터(예: ; charset=) 제거 또는 Spring MediaType 파싱 기반 비교로 허용 여부를 판단하도록 개선하는 것을 권장합니다.

Copilot uses AI. Check for mistakes.
parameterWithName("uploadSize").description("업로드할 이미지의 사이즈 ( 이미지당 1개 )"),
parameterWithName("contentType")
.description(
"업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4)")
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presigned PUT URL에 withContentType(contentType)을 포함하면, 실제 업로드 요청에서도 동일한 Content-Type 헤더를 반드시 설정해야 서명 검증이 통과합니다. 현재 설명에는 이 제약이 드러나지 않으므로, 문서(description)에 “업로드(PUT) 시 동일한 Content-Type 헤더를 포함해야 함”을 명시하는 것이 필요합니다.

Suggested change
"업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4)")
"업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4). "
+ "Presigned PUT URL 발급 시 지정한 값과 동일한 Content-Type 헤더를 업로드(PUT) 요청에도 반드시 포함해야 합니다.")

Copilot uses AI. Check for mistakes.
parameterWithName("rootPath").description("업로드 경로 (예: admin/banner, admin/alcohol)"),
parameterWithName("uploadSize").description("발급할 URL 개수")
parameterWithName("uploadSize").description("발급할 URL 개수"),
parameterWithName("contentType").description("업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4)").optional()
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

product-api 문서와 동일하게, presigned URL로 PUT 업로드 시 Content-Type 헤더가 서명에 포함되는 제약을 사용자가 이해할 수 있도록 description에 “업로드 요청 시 동일한 Content-Type 헤더 필요”를 명시해 주세요.

Suggested change
parameterWithName("contentType").description("업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4)").optional()
parameterWithName("contentType").description("업로드 파일의 Content-Type (image/jpeg, image/png, image/webp, video/mp4), 업로드 요청 시 동일한 Content-Type 헤더 필요").optional()

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +8
EXPIRY_TIME_RANGE_INVALID(HttpStatus.BAD_REQUEST, "만료 기간의 범위가 적절하지 않습니다.( 최소 1분 ,최대 10분) "),
UNSUPPORTED_CONTENT_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 Content-Type입니다.");
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EXPIRY_TIME_RANGE_INVALID 메시지에 불필요한 공백(예: "( 최소 1분 ,최대 10분) ")이 포함되어 가독성이 떨어집니다. 또한 UNSUPPORTED_CONTENT_TYPE는 어떤 값이 거절되었는지/무엇이 허용되는지 정보가 없어 디버깅이 어려우니, 가능하다면 거절된 contentType과 허용 목록을 메시지/에러 상세(예: exception detail)로 포함시키는 개선을 권장합니다.

Suggested change
EXPIRY_TIME_RANGE_INVALID(HttpStatus.BAD_REQUEST, "만료 기간의 범위가 적절하지 않습니다.( 최소 1분 ,최대 10분) "),
UNSUPPORTED_CONTENT_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 Content-Type입니다.");
EXPIRY_TIME_RANGE_INVALID(HttpStatus.BAD_REQUEST, "만료 기간의 범위가 적절하지 않습니다.(최소 1분, 최대 10분)"),
UNSUPPORTED_CONTENT_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 Content-Type입니다. 요청하신 Content-Type과 허용되는 목록을 확인해주세요.");

Copilot uses AI. Check for mistakes.
- admin 통합 테스트 업로드 시 Content-Type을 image/jpeg로 변경 (서명 불일치 해소)
- MIME 타입 lookup 전 trim/소문자 정규화 및 파라미터 제거 처리 추가
- RestDocs 설명에 PUT 요청 시 동일 Content-Type 헤더 필요 명시
- FakeAmazonS3에서 불필요한 System.out.println 제거

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Whale0928 Whale0928 merged commit aa824b7 into main Mar 24, 2026
8 checks passed
@Whale0928 Whale0928 deleted the feat/presign-url branch March 24, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants