-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 사진 presignedUrl 발급 및 업로드 보고 로직 구현 #24
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
Conversation
WalkthroughS3 SDK 의존성 추가, S3Client·S3Presigner 빈 등록, 프리사인드 URL 발급 서비스 및 사진 업로드 워크플로우(요청 검증·엔티티 생성·상태 전이·앨범 카운트 증감·업로드 결과 보고)와 관련 검증·예외·DTO·매퍼·저장소 쿼리·컨트롤러가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller
participant PhotoService
participant AlbumValidator
participant PhotoValidator
participant PhotoRepo
participant AlbumRepo
participant PresignedUrlService
participant S3
rect rgb(242,248,255)
Note over Client,Controller: 프리사인드 URL 생성 (요청→검증→엔티티 저장→카운트 증가→프리사인드 발급)
Client->>Controller: POST /v1/photo/presigned-url
Controller->>PhotoService: createPresignedUrls(user, req)
PhotoService->>AlbumValidator: validateUploadPermission(album, user)
PhotoService->>PhotoValidator: validateFileInfos(...)
PhotoService->>PhotoRepo: save(Photo status=UPLOADING) [N]
PhotoService->>AlbumRepo: incrementPhotoCount(albumId, N)
loop per file
PhotoService->>PresignedUrlService: generatePresignedPutUrl(key, contentType)
PresignedUrlService->>S3: presign PUT (10m)
S3-->>PresignedUrlService: presigned URL
end
PhotoService-->>Controller: PhotoPresignedUrlResponse
Controller-->>Client: 200 OK
end
rect rgb(242,255,242)
Note over Client,Controller: 업로드 결과 보고 (성공/실패 업데이트·롤백)
Client->>Controller: POST /v1/photo/report
Controller->>PhotoService: reportUploadResult(user, req)
PhotoService->>PhotoValidator: validatePhotos(userId, ids)
PhotoService->>PhotoRepo: updateStatusByIds(successIds, UPLOADING→PROCESSING)
PhotoService->>PhotoRepo: updateStatusByIds(failureIds, UPLOADING→FAILED)
PhotoService->>AlbumRepo: decrementPhotoCount(albumId, failureCount)
PhotoService-->>Controller: void
Controller-->>Client: 200 OK
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 7
🧹 Nitpick comments (17)
src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (1)
71-78: 코드 중복을 줄이기 위한 리팩토링을 권장합니다.
validateUploadPermission메서드는validateAlbumEntry메서드(lines 63-69)와 동일한 검증 로직(만료 확인, 블랙리스트 확인)을 중복 구현하고 있습니다.다음 중 하나의 방식으로 리팩토링을 고려해보세요:
방법 1: validateAlbumEntry를 재사용
public void validateUploadPermission(Album album, User user) { - validateAlbumExpiration(album); - - validateUserBlacklisted(album, user); + validateAlbumEntry(album, user); albumParticipantRepository.findByUserIdAndAlbumId(user.getId(), album.getId()) .orElseThrow(() -> new AlbumException(AlbumErrorCode.USER_NOT_PARTICIPANT)); }방법 2: 공통 검증 메서드 추출
private void validateBasicPermission(Album album, User user) { validateAlbumExpiration(album); validateUserBlacklisted(album, user); } public void validateAlbumEntry(Album album, User user) { validateBasicPermission(album, user); } public void validateUploadPermission(Album album, User user) { validateBasicPermission(album, user); albumParticipantRepository.findByUserIdAndAlbumId(user.getId(), album.getId()) .orElseThrow(() -> new AlbumException(AlbumErrorCode.USER_NOT_PARTICIPANT)); }src/main/java/com/cheeeese/global/config/S3Config.java (1)
17-27: 주입된 프로퍼티의 유효성 검증 추가를 권장합니다.현재
@Value로 주입되는 설정값들에 대한 검증이 없어, 잘못된 값이 주입될 경우 런타임에 실패할 수 있습니다. 특히region값이 유효하지 않으면Region.of(region)에서 예외가 발생합니다.
@PostConstruct를 사용해 빈 초기화 시점에 필수 값들을 검증하는 로직을 추가하는 것을 고려해보세요:@PostConstruct public void validateConfiguration() { if (accessKey == null || accessKey.isBlank()) { throw new IllegalStateException("NCP access key must be configured"); } if (secretKey == null || secretKey.isBlank()) { throw new IllegalStateException("NCP secret key must be configured"); } if (endpoint == null || endpoint.isBlank()) { throw new IllegalStateException("NCP endpoint must be configured"); } if (region == null || region.isBlank()) { throw new IllegalStateException("NCP region must be configured"); } }src/main/java/com/cheeeese/photo/exception/PhotoException.java (1)
7-12: @Getter 어노테이션이 불필요할 수 있습니다.이 클래스에는 선언된 필드가 없으므로
@Getter어노테이션이 현재로서는 효과가 없습니다. 다만BusinessException의 상속된 필드를 위한 것이거나 향후 확장을 위한 것이라면 유지해도 무방합니다.build.gradle (1)
58-60: AWS SDK 버전 업데이트를 권장합니다.현재 사용 중인 AWS SDK 2.25.46 버전은 최신 버전 2.34.0에서 9개 버전 뒤쳐져 있습니다. 다행히 이 버전에서는 보고된 보안 취약점이 없으나, 버그 수정, 성능 개선, 유지보수 목적으로 최신 버전으로 업데이트할 것을 권장합니다:
implementation 'software.amazon.awssdk:s3:2.34.0' implementation 'software.amazon.awssdk:auth:2.34.0' implementation 'software.amazon.awssdk:regions:2.34.0'src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1)
12-21: 업로드 결과 보고 중 충돌 케이스용 에러 코드 추가 제안
successPhotoIds와failurePhotoIds가 겹치는 경우를 식별할 별도 에러 코드가 필요합니다(현재 서비스 레벨에서 검증·차단 권장). 아래 상수 추가를 제안드립니다.PHOTO_ID_NOT_FOUND(HttpStatus.NOT_FOUND, "보고된 사진 ID 중 존재하지 않는 ID가 포함되어 있습니다."), + PHOTO_REPORT_CONFLICTING_IDS(HttpStatus.BAD_REQUEST, "successPhotoIds와 failurePhotoIds에 중복된 ID가 포함되어 있습니다."), ;src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
18-20: 시간 소스 주입으로 테스트 가능성과 일관성 개선
LocalDateTime.now()대신Clock주입 후LocalDateTime.now(clock)사용을 권장합니다. 시간대/테스트 안정성 향상에 유리합니다.src/main/java/com/cheeeese/photo/application/PresignedUrlService.java (1)
29-31: Presigned URL 유효기간을 설정값으로 외부화현재 10분 하드코딩입니다. 운영/테스트 환경별 조정 위해 프로퍼티화 권장.
@RequiredArgsConstructor public class PresignedUrlService { private final S3Presigner s3Presigner; @Value("${ncp.object-storage.bucket}") private String bucket; + @Value("${photo.presign.ttl-minutes:10}") + private long ttlMinutes; public String generatePresignedPutUrl(String uniqueKey, String contentType) { PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucket) .key(uniqueKey) .contentType(contentType) .build(); PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(p -> p - .signatureDuration(Duration.ofMinutes(10)) + .signatureDuration(Duration.ofMinutes(ttlMinutes)) .putObjectRequest(putObjectRequest) );src/main/java/com/cheeeese/photo/dto/request/PhotoUploadReportRequest.java (1)
10-19: 계약 명시: 두 목록의 ‘겹침 금지’와 중복 처리 방침문서에 “success/failure는 서로 겹치면 안 됨”을 명시하고(서비스에서 검증), 중복 ID 허용 여부도 정의하세요. 현 구현은 중복 시 카운트 왜곡 위험이 큽니다.
-@Schema(description = "사진 업로드 결과 보고 요청 (부분 성공/실패 처리)") +@Schema(description = "사진 업로드 결과 보고 요청 (부분 성공/실패 처리). " + + "successPhotoIds와 failurePhotoIds는 서로 겹치면 안 됩니다(겹치면 400).") public record PhotoUploadReportRequest( @@ - @Schema(description = "업로드가 성공적으로 완료된 사진 ID 목록 (UPLOADING -> PROCESSING)", example = "[100, 102]") + @Schema(description = "업로드가 성공적으로 완료된 사진 ID 목록 (UPLOADING -> PROCESSING). 중복은 무시 또는 400으로 명시.", example = "[100, 102]") List<Long> successPhotoIds, @@ - @Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백)", example = "[101, 103]") + @Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백). success 목록과 겹치면 안 됨.", example = "[101, 103]") List<Long> failurePhotoIdssrc/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (2)
23-35: 문서 강화: URL 유효기간과 업로드 시 헤더 요구사항 명시
- Presigned URL 유효기간(기본 10분, 설정화 시 해당 값)을 명시.
- 업로드 시 서명에 포함된
Content-Type헤더를 반드시 동일하게 전송해야 함을 명시.- description = """ + description = """ ### RequestBody --- `albumCode`: 사진을 업로드할 앨범의 코드 \n `fileInfos`: 업로드할 파일 정보 목록 (파일명, 크기, Content-Type) \n ### 로직 상세 --- 1. 앨범의 존재 및 만료 여부 확인 2. 앨범의 최대 사진 개수 (`maxPhotoCount`) 초과 여부 확인 3. 파일별 크기(6MB), Content-Type(image/*) 유효성 검증 4. 검증 통과 시, DB에 `Photo` 레코드를 `UPLOADING` 상태로 생성 - 5. 클라우드 스토리지 Presigned URL을 발급하여 반환 + 5. 클라우드 스토리지 Presigned URL을 발급하여 반환 (URL 유효기간: 기본 10분) \n + 6. 업로드 시 요청의 `Content-Type` 헤더는 발급 시 지정된 값과 동일해야 함 """
79-88: 보고 API 문서에 재실행(idempotency) 및 겹침 금지 명시동일 ID를 여러 번 보고해도 안전해야 함(특히 실패 재보고). success/failure 겹침 금지와 중복 처리 방침을 여기에 명시해 주세요.
src/main/java/com/cheeeese/photo/application/PhotoService.java (1)
123-134: 성공 전이 결과 확인(선택): 영향 행 수 점검상태 전이 업데이트의 영향 행 수를 확인해 불일치 시 로깅/모니터링을 권장합니다(검증 우회나 중간 상태 변경 감지).
- photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus( + int updated = photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus( successPhotoIds, userId, PhotoStatus.PROCESSING, PhotoStatus.UPLOADING ); + // TODO: updated != successPhotoIds.size() 시 경고 로그/메트릭 적재 권장src/main/java/com/cheeeese/photo/presentation/PhotoController.java (2)
26-32: 요청/응답 콘텐츠 타입을 명시하고 검증 활성화 권장API 계약을 안정화하려면 consumes/produces를 명시하세요. 클래스에 @validated 추가도 고려.
import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; -@RestController -@RequiredArgsConstructor +@RestController +@RequiredArgsConstructor +@Validated @RequestMapping("/v1/photo") public class PhotoController implements PhotoSwagger { - @PostMapping("/presigned-url") + @PostMapping( + value = "/presigned-url", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) public CommonResponse<PhotoPresignedUrlResponse> createPresignedUrls(
35-42: /report 엔드포인트는 멱등성 보장 필요클라이언트 재시도(네트워크 타임아웃/브라우저 재전송) 시 중복 반영/중복 롤백 위험. 업로드 배치/리포트 식별자(예: issuanceId, uploadId, or ETag)로 멱등 키를 강제하고, DB에 유니크 제약을 두거나 “이미 처리됨”을 안전하게 반환하도록 설계하세요. 서비스 레벨에서 트랜잭션 경계도 점검 바랍니다.
src/main/java/com/cheeeese/photo/dto/request/PhotoPresignedUrlRequest.java (2)
13-20: DTO 유효성 강화: albumCode 공백 금지, 파일 목록 최소 1개, 요소 검증 전파현재 @NotNull 만으로는 ""(빈 문자열) 및 빈 리스트가 통과합니다. 리스트 요소에 대한 @Valid 전파도 필요합니다.
-import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import jakarta.validation.Valid @Builder @Schema(description = "Presigned URL 발급 요청") public record PhotoPresignedUrlRequest( - @NotNull + @NotBlank @Schema(description = "앨범 코드", example = "786ccd09-5f22-4aa9-a32b-f62dd2e94cc8") String albumCode, - @NotNull - @Schema(description = "업로드할 파일 정보 목록") - List<FileInfo> fileInfos + @NotNull + @Size(min = 1) + @Schema(description = "업로드할 파일 정보 목록(최소 1개)") + List<@Valid FileInfo> fileInfos ) {
21-34: fileSize 하한 검증 추가 및 예시 명확화0 또는 음수 크기를 조기에 차단하세요. Validator에서도 상한만 체크하므로 하한을 DTO에서 보완하면 UX가 좋아집니다.
-import lombok.Builder; +import lombok.Builder; +import jakarta.validation.constraints.Positive; @Builder @Schema(description = "개별 파일 정보") public record FileInfo( @NotBlank @Schema(description = "원본 파일명", example = "my_holiday_pic.jpg") String fileName, - @Schema(description = "파일 크기 (Byte)", example = "3000000") - long fileSize, + @Positive + @Schema(description = "파일 크기 (Byte, >0)", example = "3000000") + long fileSize, @NotBlank @Schema(description = "파일 Content-Type", example = "image/jpeg") String contentType ) {}src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java (2)
23-25: Content-Type 검사: 대소문자/공백을 정규화하고 Set으로 검사클라이언트별로 대소문자/공백 차이가 있습니다. 소문자 트림 후 검사하고, contains 성능/의도를 위해 Set 사용을 권장합니다.
-import java.util.List; +import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; - private static final long MAX_FILE_SIZE = 6 * 1024 * 1024; // 6MB - private static final List<String> ALLOWED_TYPES = List.of("image/jpeg", "image/png", "image/jpg"); + private static final long MAX_FILE_SIZE = 6 * 1024 * 1024; // 6MB + private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/png", "image/jpg"); @@ - if (!ALLOWED_TYPES.contains(file.contentType())) { + String ct = file.contentType() == null ? null : file.contentType().trim().toLowerCase(Locale.ROOT); + if (ct == null || !ALLOWED_TYPES.contains(ct)) { throw new PhotoException(PhotoErrorCode.PHOTO_INVALID_CONTENT_TYPE); }Also applies to: 43-45
29-47: 파일 목록 요소 null, 크기 하한 미검증요소가 null이면 NPE 발생 위험. 파일 크기 0/음수 허용도 비정상 흐름을 유발할 수 있습니다. 조기 검증을 추가하세요.
public void validateFileInfos(List<PhotoPresignedUrlRequest.FileInfo> fileInfos) { if (fileInfos == null || fileInfos.isEmpty()) { throw new PhotoException(PhotoErrorCode.PHOTO_FILE_LIST_EMPTY); } - for (PhotoPresignedUrlRequest.FileInfo file : fileInfos) { + for (PhotoPresignedUrlRequest.FileInfo file : fileInfos) { + if (file == null) { + // 전용 에러코드가 없으면 PHOTO_FILE_NAME_REQUIRED 대신 별도 코드 추가를 권장 + throw new PhotoException(PhotoErrorCode.PHOTO_FILE_NAME_REQUIRED); + } if (file.fileName() == null || file.fileName().isBlank()) { throw new PhotoException(PhotoErrorCode.PHOTO_FILE_NAME_REQUIRED); } - if (file.fileSize() > MAX_FILE_SIZE) { + if (file.fileSize() <= 0 || file.fileSize() > MAX_FILE_SIZE) { throw new PhotoException(PhotoErrorCode.PHOTO_FILE_SIZE_EXCEEDED); } - if (!ALLOWED_TYPES.contains(file.contentType())) { + // 대소문자/공백 정규화는 상단 코멘트의 diff 참고 + if (!ALLOWED_TYPES.contains(file.contentType())) { throw new PhotoException(PhotoErrorCode.PHOTO_INVALID_CONTENT_TYPE); } } }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
build.gradle(1 hunks)src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java(1 hunks)src/main/java/com/cheeeese/album/exception/code/AlbumErrorCode.java(1 hunks)src/main/java/com/cheeeese/album/infrastructure/persistence/AlbumRepository.java(1 hunks)src/main/java/com/cheeeese/global/common/code/SuccessCode.java(1 hunks)src/main/java/com/cheeeese/global/config/S3Config.java(1 hunks)src/main/java/com/cheeeese/photo/application/PhotoService.java(2 hunks)src/main/java/com/cheeeese/photo/application/PresignedUrlService.java(1 hunks)src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java(1 hunks)src/main/java/com/cheeeese/photo/domain/Photo.java(2 hunks)src/main/java/com/cheeeese/photo/domain/PhotoStatus.java(1 hunks)src/main/java/com/cheeeese/photo/dto/request/PhotoPresignedUrlRequest.java(1 hunks)src/main/java/com/cheeeese/photo/dto/request/PhotoUploadReportRequest.java(1 hunks)src/main/java/com/cheeeese/photo/dto/response/PhotoPresignedUrlResponse.java(1 hunks)src/main/java/com/cheeeese/photo/exception/PhotoException.java(1 hunks)src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java(1 hunks)src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java(1 hunks)src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java(2 hunks)src/main/java/com/cheeeese/photo/presentation/PhotoController.java(1 hunks)src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/cheeeese/photo/application/PhotoService.java (1)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
PhotoMapper(11-40)
src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1)
src/main/java/com/cheeeese/photo/exception/PhotoException.java (1)
Getter(7-12)
🔇 Additional comments (9)
src/main/java/com/cheeeese/album/exception/code/AlbumErrorCode.java (1)
24-24: LGTM!새로운 에러 코드 추가가 깔끔합니다. FORBIDDEN 상태 코드와 명확한 에러 메시지가 적절하게 설정되었습니다.
src/main/java/com/cheeeese/global/common/code/SuccessCode.java (1)
30-31: LGTM!새로운 성공 코드가 명확하게 정의되어 있습니다. 메시지도 직관적이고 일관성 있습니다.
src/main/java/com/cheeeese/global/config/S3Config.java (1)
30-40: 자격 증명 로테이션 시 재시작이 필요함을 인지하세요.
StaticCredentialsProvider를 사용하면 애플리케이션 시작 시점에 자격 증명이 한 번만 로드되므로, NCP Object Storage의 액세스 키를 로테이션할 경우 애플리케이션을 재시작해야 합니다. 현재 구현은 단순하고 명확하지만, 향후 무중단 자격 증명 로테이션이 필요하다면 동적 자격 증명 프로바이더로 전환을 고려해야 합니다.src/main/java/com/cheeeese/photo/dto/response/PhotoPresignedUrlResponse.java (1)
8-23: LGTM!Record 기반 DTO 구조가 깔끔하고, Swagger 문서화도 잘 되어 있습니다. 중첩된
PresignedUrlInfo레코드도 적절하게 구성되었습니다.src/main/java/com/cheeeese/photo/domain/PhotoStatus.java (1)
3-8: LGTM!사진의 생명주기를 명확하게 표현한 상태 열거형입니다. PR에서 언급한 낙관적 증가 방식(optimistic increment)과 실패 시 롤백 로직에 잘 부합합니다.
src/main/java/com/cheeeese/photo/domain/Photo.java (3)
31-31: LGTM!
imageUrl을 nullable로 변경한 것은 PR에서 언급한 낙관적 증가 방식과 잘 부합합니다. 업로드 전에 Photo 엔티티를 생성하고, 업로드 완료 후 URL을 업데이트하는 전략이 명확히 드러납니다.
43-45: LGTM!
PhotoStatus를EnumType.STRING으로 저장하는 것은 좋은 선택입니다. 향후 enum 순서가 변경되거나 새로운 상태가 추가되어도 데이터베이스의 기존 데이터가 영향을 받지 않습니다.
64-66: LGTM!업로드 완료 후 이미지 URL을 업데이트하기 위한 메서드가 명확하게 구현되어 있습니다. 도메인 주도 설계 원칙에 따라 엔티티의 상태를 캡슐화하여 변경하는 좋은 접근입니다.
src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (1)
3-3: CommonResponse 임포트 경로 확인 완료 - 이슈 없음코드베이스 검증 결과,
com.cheeeese.global.common.CommonResponse임포트 경로가 정확합니다. CommonResponse 클래스는src/main/java/com/cheeeese/global/common/CommonResponse.java에 위치하며, 동일한 임포트 패턴이 UserSwagger.java, AuthSwagger.java, UserController.java 등 다른 파일에서도 일관되게 사용되고 있습니다. 대체 경로(common.dto.CommonResponse)는 존재하지 않습니다.
src/main/java/com/cheeeese/album/infrastructure/persistence/AlbumRepository.java
Show resolved
Hide resolved
src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java
Show resolved
Hide resolved
src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java
Show resolved
Hide resolved
zyovn
left a comment
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.
수고하셨습니다~~ 🧀
photoService 내에 메서드가 굉장히 많고 추후에도 계속 늘어날 것 같아서 나중에 분리하면 조을 것 같아요! 나중에 같이 얘기 해봅시당
🔗 연관된 이슈
🚀 변경 유형
📝 작업 내용
📸 스크린샷
presigned-url 발급 성공

최대 사진 개수 초과

6MB 파일 크기 초과

파일명 누락

이미지 형식 불일치

빈 업로드 파일 목록

report 성공

존재하지 않는 ID

소유자 불일치

동일 앨범 아님

💬 리뷰 요구사항
Summary by CodeRabbit
New Features
Chores