Conversation
This reverts commit 15ca516.
📝 WalkthroughWalkthroughS3 presigned URL 기반 이력서·자소서 파일 업로드 기능을 추가했습니다. 파일 형식·카테고리 모델, 업로드 확인·조회·삭제 서비스, API 엔드포인트와 AWS S3 설정을 구현했습니다. Changes이력서 파일 관리
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ResumeController
participant ResumeService
participant S3Client
participant ResumeRepository
Client->>ResumeController: presigned URL 요청
ResumeController->>ResumeService: 사용자 ID와 요청 전달
ResumeService->>S3Client: PUT URL 생성
S3Client-->>ResumeService: presigned URL 반환
ResumeService-->>ResumeController: URL과 objectKey 반환
Client->>S3Client: 파일 업로드
Client->>ResumeController: 업로드 확인 요청
ResumeController->>ResumeService: objectKey와 파일 메타데이터 전달
ResumeService->>S3Client: HeadObject로 파일 검증
ResumeService->>ResumeRepository: Resume 생성 또는 갱신
ResumeRepository-->>ResumeService: fileId 반환
ResumeService-->>ResumeController: 업로드 확인 응답
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)src/main/resources/application.ymlTraceback (most recent call last): 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.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/main/java/com/leets7th/job_is_be/domain/user/entity/Resume.java (1)
43-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value컬럼명과 필드명 불일치.
fileFormat필드가file_type컬럼에 매핑되어 있습니다.category(→category),fileName(→file_name),s3Key(→s3_key),uploadedAt(→uploaded_at)은 모두 필드명과 컬럼명이 일치하는데fileFormat만 예외적입니다. 이전fileType컬럼을 재사용한 흔적으로 보이며, 스키마를 직접 조회하는 사람에게 혼란을 줄 수 있습니다.♻️ 제안
`@Enumerated`(EnumType.STRING) - `@Column`(name = "file_type", nullable = false, length = 10) + `@Column`(name = "file_format", nullable = false, length = 10) private ResumeFileFormat fileFormat;🤖 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/leets7th/job_is_be/domain/user/entity/Resume.java` around lines 43 - 45, Update the `@Column` mapping on Resume.fileFormat so its column name matches the field name convention, replacing the legacy file_type name with file_format. Keep the enum mapping and other column attributes unchanged.src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java (1)
82-114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win동시 요청 시 unique 제약 위반으로 500 발생 가능.
findByUserAndCategory조회와save/replace사이에 TOCTOU 윈도우가 있어, 동일 user+category에 대해 동시에 두 요청이 들어오면(user_id, category)unique 제약을 위반하는DataIntegrityViolationException이 그대로 전파되어 클라이언트에 일반화되지 않은 500 오류가 노출됩니다. 흔한 시나리오는 아니지만, 잡아서GeneralException으로 변환하거나 재조회 후 재시도하는 처리가 있으면 더 견고합니다.🤖 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/leets7th/job_is_be/domain/user/service/ResumeService.java` around lines 82 - 114, Update confirmUpload around findByUserAndCategory and resumeRepository.save to handle concurrent insert races: catch DataIntegrityViolationException caused by the user/category unique constraint, then re-fetch the existing resume and apply the upload update or translate the conflict to the appropriate GeneralException. Ensure the unique-constraint violation is not propagated as an unhandled 500 response.
🤖 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/leets7th/job_is_be/domain/user/service/ResumeService.java`:
- Around line 124-132: Update deleteFile so resumeRepository.delete(resume) is
executed before deleteObject(resume.getS3Key()). Preserve the existing
user/resume validation, and perform S3 deletion only after the database deletion
is successfully committed, using the existing cleanup mechanism if the object
deletion fails.
- Around line 58-80: Update issuePresignedUrl and parseFileFormat so the file
extension determines the allowed MIME type on the server. Validate or map the
extension to its corresponding MIME type, reject unsupported or mismatched
request.contentType values, and pass the server-derived MIME type to
PutObjectRequest.contentType instead of the client-supplied value.
In `@src/main/java/com/leets7th/job_is_be/global/status/SuccessStatus.java`:
- Around line 41-44: Update the resume upload confirmation flow using
RESUME_UPLOAD_CONFIRM_SUCCESS so re-uploads that update an existing Resume
record do not return 201 CREATED; distinguish create versus update and select
the appropriate status, or consistently use 200 OK if the operation is an
upsert.
In `@src/main/resources/application.yml`:
- Around line 34-38: S3 설정을 fail-fast 검증으로 변경하세요.
src/main/resources/application.yml 34-38의 AWS_S3_BUCKET 기본값을 제거하고,
src/main/java/com/leets7th/job_is_be/global/properties/AwsS3Properties.java
7-11의 AwsS3Properties에 `@Validated를` 적용한 뒤 bucket에는 `@NotBlank`,
presigned-url-expiration에는 `@Positive` 검증을 추가하세요.
---
Nitpick comments:
In `@src/main/java/com/leets7th/job_is_be/domain/user/entity/Resume.java`:
- Around line 43-45: Update the `@Column` mapping on Resume.fileFormat so its
column name matches the field name convention, replacing the legacy file_type
name with file_format. Keep the enum mapping and other column attributes
unchanged.
In `@src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java`:
- Around line 82-114: Update confirmUpload around findByUserAndCategory and
resumeRepository.save to handle concurrent insert races: catch
DataIntegrityViolationException caused by the user/category unique constraint,
then re-fetch the existing resume and apply the upload update or translate the
conflict to the appropriate GeneralException. Ensure the unique-constraint
violation is not propagated as an unhandled 500 response.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ceef3626-b830-4025-9cfc-c5b12a92c564
📒 Files selected for processing (18)
.gitignorebuild.gradlesrc/main/java/com/leets7th/job_is_be/domain/user/controller/ResumeController.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/PresignedUrlRequest.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/PresignedUrlResponse.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/ResumeConfirmRequest.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/ResumeResponse.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/ResumeUploadResponse.javasrc/main/java/com/leets7th/job_is_be/domain/user/entity/Resume.javasrc/main/java/com/leets7th/job_is_be/domain/user/enums/ResumeCategory.javasrc/main/java/com/leets7th/job_is_be/domain/user/enums/ResumeFileFormat.javasrc/main/java/com/leets7th/job_is_be/domain/user/repository/ResumeRepository.javasrc/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.javasrc/main/java/com/leets7th/job_is_be/global/config/S3Config.javasrc/main/java/com/leets7th/job_is_be/global/properties/AwsS3Properties.javasrc/main/java/com/leets7th/job_is_be/global/status/ErrorStatus.javasrc/main/java/com/leets7th/job_is_be/global/status/SuccessStatus.javasrc/main/resources/application.yml
| RESUME_PRESIGNED_URL_SUCCESS(HttpStatus.OK, "RESUME_200_1", "업로드용 Presigned URL을 발급했습니다."), | ||
| RESUME_UPLOAD_CONFIRM_SUCCESS(HttpStatus.CREATED, "RESUME_201_1", "이력서/자소서 파일을 저장했습니다."), | ||
| RESUME_LIST_SUCCESS(HttpStatus.OK, "RESUME_200_2", "이력서/자소서 파일 목록을 조회했습니다."), | ||
| RESUME_DELETE_SUCCESS(HttpStatus.OK, "RESUME_200_3", "이력서/자소서 파일을 삭제했습니다."); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
재업로드 시 201을 반환하는 계약을 수정하세요.
동일 카테고리 재업로드는 기존 Resume 레코드 갱신이지만, 컨트롤러는 항상 RESUME_UPLOAD_CONFIRM_SUCCESS를 사용해 201 CREATED를 반환합니다. 생성과 갱신을 구분해 상태 코드를 선택하거나, upsert API라면 200 OK를 일관되게 사용하세요.
🤖 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/leets7th/job_is_be/global/status/SuccessStatus.java` around
lines 41 - 44, Update the resume upload confirmation flow using
RESUME_UPLOAD_CONFIRM_SUCCESS so re-uploads that update an existing Resume
record do not return 201 CREATED; distinguish create versus update and select
the appropriate status, or consistently use 200 OK if the operation is an
upsert.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java (1)
81-91: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPresigned 요청 구성도 검증하세요.
현재 반환값만 확인하므로 잘못된 bucket, key, Content-Type 또는 만료 시간이
PutObjectPresignRequest에 전달돼도 테스트가 통과합니다. 인자를 캡처해 설정값을 검증하세요.제안
+import org.mockito.ArgumentCaptor; + ... assertThat(response.expiresIn()).isEqualTo(300L); + + ArgumentCaptor<PutObjectPresignRequest> captor = + ArgumentCaptor.forClass(PutObjectPresignRequest.class); + verify(s3Presigner).presignPutObject(captor.capture()); + + PutObjectPresignRequest actual = captor.getValue(); + assertThat(actual.putObjectRequest().bucket()).isEqualTo("test-bucket"); + assertThat(actual.putObjectRequest().key()).isEqualTo("profile/1/RESUME"); + assertThat(actual.putObjectRequest().contentType()).isEqualTo("application/pdf"); + assertThat(actual.signatureDuration()).isEqualTo(Duration.ofMinutes(5));🤖 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/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java` around lines 81 - 91, Update the issuePresignedUrl test around resumeService.issuePresignedUrl to capture the PutObjectPresignRequest passed to s3Presigner.presignPutObject, then assert its bucket, object key, content type, and expiration settings match the expected values. Keep the existing response assertions unchanged.
🤖 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.
Nitpick comments:
In
`@src/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java`:
- Around line 81-91: Update the issuePresignedUrl test around
resumeService.issuePresignedUrl to capture the PutObjectPresignRequest passed to
s3Presigner.presignPutObject, then assert its bucket, object key, content type,
and expiration settings match the expected values. Keep the existing response
assertions unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ea76a025-5d95-4403-a693-c3341939da57
📒 Files selected for processing (1)
src/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java
jihoonkim501
left a comment
There was a problem hiding this comment.
수고 많으셨습니다~! 코드레빗 수정사항만 수정 부탁드릴게요~!~!
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java (1)
102-119: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winS3/DB 일관성 보상 및 재시도 경로를 추가하세요.
confirmUpload()에서 DB save/update 실패 시 이미 업로드된 S3 객체가 유지되면 재확인·재업로드 경로가 없어 orphan이 남을 수 있습니다.deleteFile()의afterCommit/deleteObject 실패도 DB 삭제만 적용되고 객체가 남으므로, DELETE를 재시도하거나 Durable outbox로 cleanup 상태를 기록해 복구할 수 있도록 보상해야 합니다.🤖 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/leets7th/job_is_be/domain/user/service/ResumeService.java` around lines 102 - 119, confirmUpload()에서 DB 저장·수정 실패 시 이미 업로드된 S3 객체를 정리하고 재확인·재업로드할 수 있도록 보상 및 재시도 경로를 추가하세요. deleteFile()의 afterCommit 및 deleteObject 실패도 삭제 상태를 유실하지 않도록 DELETE 재시도 또는 Durable outbox에 cleanup 작업을 기록해 복구 가능하게 하세요. DB와 S3 작업 결과가 어느 한쪽에만 남지 않도록 기존 성공 흐름은 유지하세요.
🤖 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/leets7th/job_is_be/domain/user/service/ResumeService.java`:
- Around line 63-68: Update issuePresignedUrl() so each upload session generates
and persists a unique, non-reusable identifier instead of using only userId and
category in buildObjectKey(). Update confirmUpload() to validate that the
requested object belongs to the active upload session and matches its stored
nonce or verifiable ETag before confirming it, preventing stale objects or
overwritten files from being accepted.
---
Outside diff comments:
In `@src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java`:
- Around line 102-119: confirmUpload()에서 DB 저장·수정 실패 시 이미 업로드된 S3 객체를 정리하고
재확인·재업로드할 수 있도록 보상 및 재시도 경로를 추가하세요. deleteFile()의 afterCommit 및 deleteObject 실패도
삭제 상태를 유실하지 않도록 DELETE 재시도 또는 Durable outbox에 cleanup 작업을 기록해 복구 가능하게 하세요. DB와
S3 작업 결과가 어느 한쪽에만 남지 않도록 기존 성공 흐름은 유지하세요.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 170bf1cc-0ecb-4aa9-a67b-7171d82e72b3
📒 Files selected for processing (11)
src/main/java/com/leets7th/job_is_be/domain/user/controller/ResumeController.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/PresignedUrlRequest.javasrc/main/java/com/leets7th/job_is_be/domain/user/dto/ResumeUploadResponse.javasrc/main/java/com/leets7th/job_is_be/domain/user/entity/Resume.javasrc/main/java/com/leets7th/job_is_be/domain/user/enums/ResumeFileFormat.javasrc/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.javasrc/main/java/com/leets7th/job_is_be/global/properties/AwsS3Properties.javasrc/main/java/com/leets7th/job_is_be/global/status/ErrorStatus.javasrc/main/java/com/leets7th/job_is_be/global/status/SuccessStatus.javasrc/main/resources/application.ymlsrc/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (7)
- src/main/java/com/leets7th/job_is_be/domain/user/dto/ResumeUploadResponse.java
- src/main/java/com/leets7th/job_is_be/domain/user/enums/ResumeFileFormat.java
- src/main/java/com/leets7th/job_is_be/global/properties/AwsS3Properties.java
- src/main/java/com/leets7th/job_is_be/global/status/ErrorStatus.java
- src/main/java/com/leets7th/job_is_be/domain/user/controller/ResumeController.java
- src/test/java/com/leets7th/job_is_be/domain/user/service/ResumeServiceTest.java
- src/main/java/com/leets7th/job_is_be/domain/user/entity/Resume.java
| String objectKey = buildObjectKey(userId, request.category()); | ||
|
|
||
| PutObjectRequest objectRequest = PutObjectRequest.builder() | ||
| .bucket(awsS3Properties.bucket()) | ||
| .key(objectKey) | ||
| .contentType(fileFormat.mimeType()) // 클라이언트 값 대신 확장자 기준 서버 매핑 MIME 사용 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching ResumeService.java:"
fd -a 'ResumeService\.java$' . || true
file="$(fd 'ResumeService\.java$' . | head -n 1 || true)"
if [ -z "${file:-}" ]; then
echo "ResumeService.java not found"
exit 0
fi
echo "Selected: $file"
wc -l "$file"
echo "--- outline ---"
ast-grep outline "$file" --view expanded || true
echo "--- lines 1-180 ---"
sed -n '1,180p' "$file" | nl -baRepository: Leets-Official/Job-is-BE
Length of output: 1152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="./src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java"
echo "--- lines 1-220 ---"
awk '{printf "%4d %s\n", NR, $0}' "$file" | sed -n '1,220p'
echo "--- related references ---"
rg -n "issuePresignedUrl|confirmUpload|buildObjectKey|resume_file|ResumeCategory|putObject|presigned|metadata|ETag|nonce|session" "${file}" . --glob '!**/build/**' --glob '!**/target/**' --glob '!**/node_modules/**' || trueRepository: Leets-Official/Job-is-BE
Length of output: 50381
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
service = Path("./src/main/java/com/leets7th/job_is_be/domain/user/service/ResumeService.java")
text = service.read_text()
keys = {
"build_fixed_object_key": "buildObjectKey(Long userId, ResumeCategory category)",
"build_fixed_impl": "buildObjectKey(Long userId, ResumeCategory category) {\n return \"profile/\" + userId + \"/\" + category.name();",
"issue_presigned": "public PresignedUrlResponse issuePresignedUrl(Long userId, PresignedUrlRequest request) {",
"confirm_signature": "public ResumeUploadResponse confirmUpload(Long userId, ResumeConfirmRequest request) {",
"key_match": "String expectedObjectKey = buildObjectKey(userId, request.category());",
"no_nonce_field": bool(re.search(r"nonce|presignedSession|sessionId|ETag", text)),
"metadata_replaced_on_existing": "existing.replace(request.fileName(), fileFormat, uploadedAt);",
"db_delete_uses_same_key": "String s3Key = resume.getS3Key();\n resumeRepository.delete(resume);\n",
}
for k, v in keys.items():
if isinstance(v, bool):
print(f"{k}: {v}")
else:
print(f"{k}: {text.find(v) != -1} at {text.find(v) + 1}")
print("\n--- deterministic flow from source ---")
import ast
class FlowVisitor(ast.NodeVisitor):
def __init__(self):
self.in_confirm = False
self.steps = []
def visit_FunctionDef(self, node):
if node.name == "confirmUpload":
self.in_confirm = True
for s in node.body:
self.steps.append((ast.get_source_segment(text, s), s.lineno))
self.in_confirm = False
self.generic_visit(node)
print(FlowVisitor().steps)
for stmt, line in FlowVisitor().steps:
if stmt.strip():
print(f"line {line}: {stmt.strip()[:220]}")
print("\nAll presignedUrl responses and objectKey usages:")
for p in list(Path(".").rglob("*.java")):
try:
data=p.read_text(errors="ignore")
except Exception:
continue
for start,end in [(data.index(s), data.index(s)+len(s)) for s in ["objectKey","presignedUrl","confirmUpload"] if s in data]:
end=min(len(data), end+120)
start=max(0, start)
print(f"{p}:{data.count(chr(10),0,start)+1}: {data[start:start+120].replace(chr(10),' ').strip()}")
PYRepository: Leets-Official/Job-is-BE
Length of output: 2494
Presigned URL 업로드마다 고유 식별자를 사용하세요.
issuePresignedUrl()에서 objectKey를 user/category로 고정하고 있어 같은 경로가 계속 재사용됩니다. 새 업로드가 실패해도 기존 S3 객체의 headObject로 confirmUpload()가 통과될 수 있고, 기존 파일명이 새 파일명으로 덮어쓰기됩니다. 업로드 세션/nonce 또는 검증 가능한 ETag를 발급/저장해 confirm에서만 해당 세션이 허용하는 업로드를 확정하도록 변경하세요.
🤖 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/leets7th/job_is_be/domain/user/service/ResumeService.java`
around lines 63 - 68, Update issuePresignedUrl() so each upload session
generates and persists a unique, non-reusable identifier instead of using only
userId and category in buildObjectKey(). Update confirmUpload() to validate that
the requested object belongs to the active upload session and matches its stored
nonce or verifiable ETag before confirming it, preventing stale objects or
overwritten files from being accepted.
#️⃣ 관련 이슈
closed #30
PR 유형
어떤 변경 사항이 있나요?
PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
🧩 작업 내용
S3Config,AwsS3Properties로 Presigned URL 발급 설정 구성Resume엔티티를 유형별(category) 1슬롯 구조로 변경 (user_id + categoryunique 제약,s3Key기반으로 파일 경로 관리)ResumeCategory,ResumeFileFormatenum 및ResumeRepository추가/api/profile/files)POST /presigned-url: 업로드용 Presigned URL 발급POST: 업로드 완료 확인 및 파일 정보 저장 (같은 카테고리 재업로드 시 기존 row 갱신)GET: 업로드된 파일 목록 조회DELETE /{fileId}: 파일 삭제ErrorStatus,SuccessStatus) 추가📸 스크린샷(선택)
📣 To Reviewers
Summary by CodeRabbit
.DS_Store를 무시하도록 구성했습니다.