Skip to content

feat: 프로젝트 파일 S3 API 구현 - #67

Merged
chazy-d merged 12 commits into
developfrom
feature/project-file-s3-api
Jul 25, 2026
Merged

feat: 프로젝트 파일 S3 API 구현#67
chazy-d merged 12 commits into
developfrom
feature/project-file-s3-api

Conversation

@chazy-d

@chazy-d chazy-d commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈 (Related Issue)


📝 작업 내용

주요 검토 파일:

  • src/main/java/com/slatto/domain/project/controller/ProjectFileController.java
  • src/main/java/com/slatto/domain/project/service/ProjectFileService.java
  • src/main/java/com/slatto/domain/project/entity/ProjectFile.java
  • src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java
  • src/main/java/com/slatto/global/storage/S3StorageService.java
  • src/main/java/com/slatto/global/storage/StorageService.java
  • src/main/java/com/slatto/global/config/S3Config.java

1. 프로젝트 파일 API 구현

프로젝트 상세의 파일 탭에서 사용할 파일 API를 추가했습니다.

  • GET /api/v1/projects/{projectId}/files
  • POST /api/v1/projects/{projectId}/files
  • PATCH /api/v1/projects/{projectId}/files/{fileId}
  • DELETE /api/v1/projects/{projectId}/files/{fileId}
  • GET /api/v1/projects/{projectId}/files/{fileId}/download

목록 조회는 keyword 파일명 검색과 cursor 기반 페이지네이션을 지원.


2. S3 저장 구조 추가

프로젝트 파일 업로드/다운로드를 S3와 연동했습니다.

  • 업로드: FE -> BE -> S3
  • 다운로드: FE -> BE -> S3 binary streaming
  • DB에는 public URL 대신 S3 객체 식별용 storageKey 저장
  • 다운로드 API는 공통 JSON 응답이 아니라 파일 attachment 응답으로 처리

기존 global/s3 초안 구조는 프로젝트 파일 도메인에서 사용할 수 있도록 global/storage 추상화와 프로젝트 파일 서비스 쪽으로 정리했습니다.


3. 프로젝트 파일 메타데이터 정리

프로젝트 파일 탭에서 필요한 메타데이터를 저장하도록 ProjectFile 구조를 정리했습니다.

  • 파일명
  • MIME 타입
  • 파일 크기
  • 파일 설명
  • S3 storageKey
  • 최종본 여부
  • 고정 시각
  • 삭제 시각

파일 삭제는 실제 row 삭제가 아니라 deletedAt 기준 soft delete로 처리합니다.


4. 파일 업로드 검증 및 권한 검증

파일 업로드 시 기본 검증을 추가했습니다.

  • 최대 파일 크기 100MB
  • 허용 파일 타입: pdf, jpg, jpeg, png, doc, docx
  • mp4는 프로젝트 파일 탭이 아니라 영상 도메인에서 처리하는 방향으로 제외

5gb는 너무 큰 것 같다고 판단하여 100mb로 고정하였습니다.

권한은 다음 기준으로 검증합니다.

  • 목록 조회, 다운로드: 프로젝트 접근 권한 필요
  • 업로드: 프로젝트 멤버 권한 필요
  • 수정, 삭제: 파일 업로더 또는 프로젝트 ADMIN 권한 필요

5. S3 설정 보정

팀 공통 S3 설정에 맞춰 로컬/배포 환경에서 사용할 수 있도록 설정을 보정했습니다.

  • cloud.aws.s3.bucket
  • AWS SDK 기본 인증 체인 사용
  • 배포 환경에서는 EC2 IAM Role 사용 가능
  • 로컬 개발에서는 환경변수 기반 인증 사용 가능

✅ 검증

  • ./gradlew compileJava
  • ./gradlew test
  • 로컬 dev 프로파일 기준 S3 업로드/다운로드/삭제 흐름 확인

💬 To Reviewers

프로젝트 파일 업로드/다운로드는 Presigned URL 방식이 아니라 백엔드 중계 방식으로 구현했습니다.

✅ PR 체크리스트

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 변경 사항에 대한 테스트를 진행했습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 프로젝트 파일 목록 조회, 업로드, 수정, 삭제 및 다운로드 기능을 제공합니다.
    • 파일 검색과 커서 기반 페이지 이동을 지원합니다.
    • 파일 설명, 고정 여부, 최종 파일 여부를 관리할 수 있습니다.
    • 파일 형식과 크기를 검증하며, 최대 100MB 파일 업로드를 지원합니다.
    • 업로더 정보와 파일 메타데이터를 조회할 수 있습니다.
  • 변경 사항

    • 프로젝트 상세 정보의 역할 목록 명칭이 roleNames로 변경되었습니다.
    • 프로젝트 생성 및 수정에서 사용자 지정 유형명 입력이 제거되었습니다.

@chazy-d chazy-d self-assigned this Jul 24, 2026
@chazy-d chazy-d added the feature 새로운 기능 추가 label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 05a28dd7-ea5a-4891-b3e2-1a38f847910e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

프로젝트 파일의 S3 업로드·다운로드·삭제 저장소와 프로젝트 파일 관리 API를 추가했습니다. 파일 메타데이터, 검증, 권한, 커서 페이징, 핀·최종 상태 변경을 구현했으며 프로젝트의 customTypeNamemyRoles 계약을 제거·변경했습니다.

Changes

프로젝트 계약 정리

Layer / File(s) Summary
프로젝트 요청·응답 및 엔티티 계약 변경
src/main/java/com/slatto/domain/project/dto/..., src/main/java/com/slatto/domain/project/entity/Project.java, src/main/java/com/slatto/domain/project/converter/ProjectConverter.java, src/main/java/com/slatto/domain/project/service/ProjectService.java
customTypeName 필드와 생성·수정 경로를 제거하고, 프로젝트 상세 응답의 역할 필드를 roleNames로 변경했습니다.

S3 저장소 추상화

Layer / File(s) Summary
S3 저장소 계약과 구현
src/main/java/com/slatto/global/storage/..., src/main/java/com/slatto/global/config/S3Config.java
StorageServiceS3StorageService를 추가해 파일 업로드·다운로드·삭제를 S3 S3Client로 처리합니다.
AWS 설정 갱신
build.gradle, .env.example
AWS SDK BOM 버전을 갱신하고 S3 버킷 설정 항목을 유지합니다.

파일 도메인과 서비스

Layer / File(s) Summary
파일 DTO·엔티티·조회 저장소
src/main/java/com/slatto/domain/project/dto/ProjectFile*, src/main/java/com/slatto/domain/project/entity/ProjectFile.java, src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java, src/main/java/com/slatto/domain/project/exception/ProjectErrorCode.java
파일 메타데이터·업로드·수정·다운로드 DTO와 엔티티 행위를 추가하고, 활성 파일 조회 및 관련 오류 코드를 정의합니다.
파일 서비스 처리
src/main/java/com/slatto/domain/project/service/ProjectFileService.java
커서 기반 목록 조회, 파일 검증, S3 저장 키 생성, 권한 확인, 업로드·수정·삭제·다운로드 처리를 구현합니다.

파일 REST API

Layer / File(s) Summary
프로젝트 파일 엔드포인트
src/main/java/com/slatto/domain/project/controller/ProjectFileController.java
프로젝트 파일 목록, 멀티파트 업로드, 수정, 삭제, 다운로드 API를 추가하고 다운로드 응답 헤더와 스트림을 구성합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProjectFileController
  participant ProjectFileService
  participant StorageService
  Client->>ProjectFileController: 프로젝트 파일 업로드 또는 다운로드 요청
  ProjectFileController->>ProjectFileService: 요청 데이터와 사용자 ID 전달
  ProjectFileService->>StorageService: 저장 키 기준 S3 업로드 또는 다운로드
  StorageService-->>ProjectFileService: 저장 완료 또는 파일 스트림 반환
  ProjectFileService-->>ProjectFileController: 파일 응답 데이터 반환
  ProjectFileController-->>Client: JSON 응답 또는 다운로드 스트림 반환
Loading

Possibly related issues

  • SLAT-TO/SLAT-TO-BE 56번: 프로젝트 파일 S3 업로드·다운로드 API와 저장 동작을 직접 구현합니다.

Possibly related PRs

  • SLAT-TO/SLAT-TO-BE#19: customTypeName 제거와 프로젝트 DTO·엔티티 경로가 직접 연결됩니다.
  • SLAT-TO/SLAT-TO-BE#40: 프로젝트 역할 목록 필드의 myRoles에서 roleNames로의 변경과 관련됩니다.
  • SLAT-TO/SLAT-TO-BE#55: 기존 S3 구성에서 StorageService·S3StorageService 구조로 전환하는 코드 흐름과 연결됩니다.

Suggested labels: feature

Suggested reviewers: sangwon02, guingguing, young0206, kohseoyoung

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning customTypeName 제거와 roleNames 변경 등 S3 이슈와 무관한 프로젝트 도메인 수정이 포함됩니다. S3 연결 관련 변경만 남기고 프로젝트 도메인 정리 작업은 별도 PR로 분리하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 프로젝트 파일 S3 API 구현이라는 제목이 주요 변경 사항과 직접 일치합니다.
Description check ✅ Passed 관련 이슈와 작업 내용은 갖췄지만 PR 체크리스트 섹션은 템플릿과 다릅니다.
Linked Issues check ✅ Passed S3Client 설정과 저장소 추상화, 업로드 기능 추가로 #54의 핵심 요구를 충족합니다.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main/java/com/slatto/global/storage/StorageService.java (1)

7-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

StorageService 계약에서 Spring/AWS 타입을 분리하세요.
Line 9-11의 MultipartFileResponseInputStream<GetObjectResponse>가 추상화 계층에 그대로 노출됩니다. ProjectFileDownloadResponse는 이미 InputStream을 쓰고 있으니, 계약은 InputStream이나 메타데이터를 담는 중립 DTO로 맞춰 구현체와 호출부의 결합을 줄여 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/slatto/global/storage/StorageService.java` around lines 7 -
13, Update the StorageService contract to remove Spring and AWS-specific types:
replace MultipartFile in upload and ResponseInputStream<GetObjectResponse> in
download with neutral abstractions such as InputStream and, where needed, a
metadata DTO. Adjust implementations and callers to adapt at the boundary while
preserving existing upload, download, and delete behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java`:
- Around line 14-28: 정렬 키와 cursor 기준이 불일치해 파일이 누락되므로, ProjectFileRepository의 조회
조건을 pinned 상태·pinnedAt 시각·id를 모두 반영하는 keyset 조건으로 변경하고 ProjectFileService의
페이지네이션 계약을 저장소 정렬 키를 담은 opaque 복합 cursor를 생성·반환하도록 갱신하세요.
ProjectFileRepository.java 14-28과 ProjectFileService.java 61-83을 모두 반영하고, 기존
pinned 우선 정렬 순서는 유지하세요.

In `@src/main/java/com/slatto/domain/project/service/ProjectFileService.java`:
- Around line 98-117: Update the upload flow in ProjectFileService around
storageService.upload() and projectFileRepository.save() so a database save or
transaction commit failure triggers guaranteed compensating deletion of the
uploaded storageKey, or persist an upload state that supports retry and cleanup.
Ensure failures cannot leave an inaccessible S3 object without a retryable or
cleanup path.
- Around line 35-45: Align the active Spring multipart configuration with
ProjectFileService.MAX_FILE_SIZE by setting
spring.servlet.multipart.max-file-size and max-request-size to at least 100MB,
ensuring multipart requests accepted by the service are not rejected earlier by
the framework.

---

Nitpick comments:
In `@src/main/java/com/slatto/global/storage/StorageService.java`:
- Around line 7-13: Update the StorageService contract to remove Spring and
AWS-specific types: replace MultipartFile in upload and
ResponseInputStream<GetObjectResponse> in download with neutral abstractions
such as InputStream and, where needed, a metadata DTO. Adjust implementations
and callers to adapt at the boundary while preserving existing upload, download,
and delete behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 904b68f9-824f-4158-b5be-6d7f33a86f8c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e71df4 and 6987501.

📒 Files selected for processing (23)
  • .env.example
  • build.gradle
  • src/main/java/com/slatto/domain/project/controller/ProjectFileController.java
  • src/main/java/com/slatto/domain/project/converter/ProjectConverter.java
  • src/main/java/com/slatto/domain/project/dto/ProjectCreateRequest.java
  • src/main/java/com/slatto/domain/project/dto/ProjectDetailResponse.java
  • src/main/java/com/slatto/domain/project/dto/ProjectFileDownloadResponse.java
  • src/main/java/com/slatto/domain/project/dto/ProjectFileListResponse.java
  • src/main/java/com/slatto/domain/project/dto/ProjectFileResponse.java
  • src/main/java/com/slatto/domain/project/dto/ProjectFileUpdateRequest.java
  • src/main/java/com/slatto/domain/project/dto/ProjectFileUploadRequest.java
  • src/main/java/com/slatto/domain/project/dto/ProjectUpdateRequest.java
  • src/main/java/com/slatto/domain/project/entity/Project.java
  • src/main/java/com/slatto/domain/project/entity/ProjectFile.java
  • src/main/java/com/slatto/domain/project/exception/ProjectErrorCode.java
  • src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java
  • src/main/java/com/slatto/domain/project/service/ProjectFileService.java
  • src/main/java/com/slatto/domain/project/service/ProjectService.java
  • src/main/java/com/slatto/global/config/S3Config.java
  • src/main/java/com/slatto/global/s3/S3Controller.java
  • src/main/java/com/slatto/global/s3/S3Service.java
  • src/main/java/com/slatto/global/storage/S3StorageService.java
  • src/main/java/com/slatto/global/storage/StorageService.java
💤 Files with no reviewable changes (5)
  • src/main/java/com/slatto/global/s3/S3Controller.java
  • src/main/java/com/slatto/domain/project/dto/ProjectCreateRequest.java
  • src/main/java/com/slatto/domain/project/dto/ProjectUpdateRequest.java
  • src/main/java/com/slatto/global/s3/S3Service.java
  • src/main/java/com/slatto/domain/project/entity/Project.java

Comment on lines +14 to +28
@Query("""
select pf
from ProjectFile pf
join fetch pf.uploader u
where pf.project.id = :projectId
and pf.deletedAt is null
and (:keyword is null
or :keyword = ''
or lower(pf.fileName) like lower(concat('%', :keyword, '%')))
and (:cursor is null or pf.id < :cursor)
order by
case when pf.pinnedAt is null then 1 else 0 end asc,
pf.pinnedAt desc,
pf.id desc
""")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

정렬 기준과 cursor 기준을 동일한 복합 키로 맞춰 주세요. pinned 파일을 먼저 정렬하면서 id만 cursor로 사용하면 항목이 누락됩니다. 예를 들어 pinned 된 낮은 id를 첫 페이지에서 반환하면, 그보다 큰 id의 unpinned 파일은 다음 조회의 pf.id < :cursor 조건에서 영구히 제외됩니다.

  • src/main/java/com/slatto/domain/project/repository/ProjectFileRepository#L14-L28: pinnedAt 상태·시각·id를 모두 반영하는 keyset 조건으로 변경하거나, id 정렬만 사용하도록 정렬 정책을 변경하세요.
  • src/main/java/com/slatto/domain/project/service/ProjectFileService#L61-L83: 저장소 정렬 키를 표현할 수 있는 opaque 복합 cursor를 생성·반환하도록 API 계약을 변경하세요.
📍 Affects 2 files
  • src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java#L14-L28 (this comment)
  • src/main/java/com/slatto/domain/project/service/ProjectFileService.java#L61-L83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/slatto/domain/project/repository/ProjectFileRepository.java`
around lines 14 - 28, 정렬 키와 cursor 기준이 불일치해 파일이 누락되므로, ProjectFileRepository의 조회
조건을 pinned 상태·pinnedAt 시각·id를 모두 반영하는 keyset 조건으로 변경하고 ProjectFileService의
페이지네이션 계약을 저장소 정렬 키를 담은 opaque 복합 cursor를 생성·반환하도록 갱신하세요.
ProjectFileRepository.java 14-28과 ProjectFileService.java 61-83을 모두 반영하고, 기존
pinned 우선 정렬 순서는 유지하세요.

Comment on lines +35 to +45
private static final long MAX_FILE_SIZE = 100L * 1024 * 1024;
private static final int DEFAULT_PAGE_SIZE = 20;
private static final int MAX_PAGE_SIZE = 50;
private static final String STORAGE_KEY_FORMAT = "projects/%d/files/%s.%s";
private static final Map<String, Set<String>> ALLOWED_EXTENSIONS_BY_CONTENT_TYPE = Map.of(
"application/pdf", Set.of("pdf"),
"image/jpeg", Set.of("jpg", "jpeg"),
"image/png", Set.of("png"),
"application/msword", Set.of("doc"),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", Set.of("docx")
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -HI '^application.*\.(yml|yaml|properties)$' . -x sh -c '
  echo "==> $1"
  rg -n "spring\.servlet\.multipart|multipart:|max-file-size|max-request-size" "$1" || true
' sh {}

Repository: SLAT-TO/SLAT-TO-BE

Length of output: 329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in src/main/resources/application.yml src/main/resources/application-dev.yml src/main/resources/application-prod.yml src/test/resources/application.yml src/main/java/com/slatto/domain/project/service/ProjectFileService.java; do
  echo "===== $f ====="
  sed -n '1,220p' "$f" | nl -ba | sed -n '1,220p'
  echo
done

Repository: SLAT-TO/SLAT-TO-BE

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [
    Path("src/main/resources/application.yml"),
    Path("src/main/resources/application-dev.yml"),
    Path("src/main/resources/application-prod.yml"),
    Path("src/test/resources/application.yml"),
    Path("src/main/java/com/slatto/domain/project/service/ProjectFileService.java"),
]:
    print(f"===== {path} =====")
    text = path.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if "multipart" in line or "MAX_FILE_SIZE" in line or "upload" in line or "storage" in line:
            print(f"{i}: {line}")
    print()
PY

Repository: SLAT-TO/SLAT-TO-BE

Length of output: 1261


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== multipart-related references =="
rg -n --hidden --glob '!target/**' --glob '!build/**' --glob '!dist/**' \
  'spring\.servlet\.multipart|max-file-size|max-request-size|MultipartConfigElement|MultipartResolver|CommonsMultipartResolver' .

echo
echo "== ProjectFileService relevant lines =="
sed -n '1,220p' src/main/java/com/slatto/domain/project/service/ProjectFileService.java | nl -ba | sed -n '1,220p'

Repository: SLAT-TO/SLAT-TO-BE

Length of output: 191


multipart 제한을 100MB 이상으로 맞춰 주세요.
MAX_FILE_SIZE는 서비스 내부 검증일 뿐이라, 활성 프로필의 spring.servlet.multipart.max-file-sizemax-request-size가 100MB 이상이어야 정상 요청이 먼저 거절되지 않습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/slatto/domain/project/service/ProjectFileService.java`
around lines 35 - 45, Align the active Spring multipart configuration with
ProjectFileService.MAX_FILE_SIZE by setting
spring.servlet.multipart.max-file-size and max-request-size to at least 100MB,
ensuring multipart requests accepted by the service are not rejected earlier by
the framework.

Comment on lines +98 to +117
String contentType = file.getContentType();
String storageKey = createStorageKey(projectId, request.getFileName());
storageService.upload(file, storageKey);

ProjectFile projectFile = ProjectFile.create(
project,
currentMember.getUser(),
request.getFileName(),
contentType,
file.getSize(),
request.getDescription(),
request.getIsFinal(),
storageKey
);

if (Boolean.TRUE.equals(request.getIsPinned())) {
projectFile.pin();
}

ProjectFile savedFile = projectFileRepository.save(projectFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

S3 업로드 성공 후 DB 실패 시 고아 객체가 남습니다.

storageService.upload()는 DB 트랜잭션 밖의 쓰기입니다. save() 또는 커밋이 실패하면 접근할 수 없는 S3 객체가 누적됩니다. 롤백 후 보상 삭제를 보장하거나, 업로드 상태를 영속화해 재시도·정리 가능한 흐름으로 바꿔 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/slatto/domain/project/service/ProjectFileService.java`
around lines 98 - 117, Update the upload flow in ProjectFileService around
storageService.upload() and projectFileRepository.save() so a database save or
transaction commit failure triggers guaranteed compensating deletion of the
uploaded storageKey, or persist an upload state that supports retry and cleanup.
Ensure failures cannot leave an inaccessible S3 object without a retryable or
cleanup path.

@chazy-d
chazy-d merged commit 10662bc into develop Jul 25, 2026
2 checks passed
@guingguing
guingguing deleted the feature/project-file-s3-api branch August 11, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: 프로젝트 파일 S3 업로드 및 다운로드 API 구현

2 participants