Skip to content

Refactor/#63 s3 presigned#66

Merged
shinae1023 merged 9 commits into
mainfrom
refactor/#63-s3-presigned
May 22, 2026
Merged

Refactor/#63 s3 presigned#66
shinae1023 merged 9 commits into
mainfrom
refactor/#63-s3-presigned

Conversation

@shinae1023
Copy link
Copy Markdown
Member

@shinae1023 shinae1023 commented May 22, 2026

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

📸 작업 화면 스크린샷

⚠️ PR하기 전에 확인해주세요

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

🚨 관련 이슈 번호 [#63]

Summary by CodeRabbit

  • New Features

    • Presigned URL endpoint for direct S3 image uploads
    • Analysis endpoints to run and retrieve automated cover-letter/job-fit analysis
  • UX / API Changes

    • Job posting extract/ingest switched to JSON with image object keys (S3) instead of multipart file uploads
  • Documentation

    • Added S3 setup guide with CORS and lifecycle policy instructions
  • Tests

    • New analysis service tests and updated job-posting tests to use image object keys

Review Change Stack

@shinae1023 shinae1023 self-assigned this May 22, 2026
@shinae1023 shinae1023 added ✨ feat New feature or request ♻️ refactor labels May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds AWS S3 presigned upload support and related S3 beans/config; refactors job-posting upload/extract/ingest to JSON requests using S3 object keys; adds JobPostingImageStorageService, presign endpoint, S3 utilities, and an LLM-based analysis domain with controllers, services, DTOs, entities, repository methods, and tests.

Changes

Repository-wide checkpoint

Layer / File(s) Summary
S3 infrastructure & configuration
build.gradle, src/main/resources/application-*.yaml, ops/s3/*, src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Config.java, src/main/java/com/jobdri/jobdri_api/global/config/s3/S3ObjectUrlService.java
Adds AWS Spring Cloud BOM and S3 starter, application AWS/S3 properties (dev/prod/test), S3 CORS and lifecycle JSON files, and Spring beans for S3Client and S3Presigner plus presign URL helpers.
S3 upload utilities
src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Uploader.java
Provides Multipart-to-temp-file upload helper and S3 deletion utility used for legacy multipart flows.
Presign DTOs & storage service
src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingImageUploadPresignRequest.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingImageUploadPresignResponse.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingImageStorageService.java
Adds presign request/response records and JobPostingImageStorageService to validate content-types, build per-user object keys, return presigned PUT URLs, validate stored objects and produce presigned GET URLs.
Presign endpoint
src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingUploadController.java
New controller POST /api/job-postings/images/presign-upload returning upload presign details for authenticated users.
JobPosting API DTOs: extract/ingest
src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractRequest.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestRequest.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java
Replaces multipart request DTOs with JSON records containing rawText and optional imageObjectKey; cross-field @AssertTrue validation enforces at least one input.
JobPosting controllers & services (refactor to object-key flow)
src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java, src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java
Controller endpoints changed from multipart @ModelAttribute to JSON @RequestBody; JobPostingAiService now accepts userId + imageObjectKey and requests readable image URLs from storage service; async/facade/ingest services updated to use imageObjectKey and removed multipart byte handling; tests updated/mocked accordingly.
Tests & test config updates
src/test/resources/application-test.yaml, src/test/java/.../JobPostingAiServiceTest.java, src/test/java/.../JobPostingIngestServiceTest.java
Adds S3 config to test profile; unit tests mock JobPostingImageStorageService and update assertions to capture/verify image object keys.
Analysis domain: API, DTOs, entities, repo, service, AI client, tests
src/main/java/com/jobdri/jobdri_api/domain/analysis/**, src/test/java/.../AnalysisServiceTest.java, src/test/java/.../QuestionServiceTest.java
Adds AnalysisController, AnalysisService, AnalysisAiClient, DTOs (AnalysisResponse, AnalysisQuestionResponse, QuestionAnalysisResponse, AnalysisLlmResponse, QuestionCandidateResponse change), entities (QuestionAnalysis status + QuestionAnalysisStatus enum, Analysis create behavior change), repository methods for question analyses, service logic to analyze via LLM, persist/replace analysis, and comprehensive tests covering analyze/get/replacement/validation flows.
Misc
src/main/java/com/jobdri/jobdri_api/domain/mockapply/entity/MockApply.java, src/main/java/com/jobdri/jobdri_api/global/apiPayload/code/GeneralErrorCode.java, src/main/resources/application.yaml
Adds clearAnalysis() on MockApply, new ANALYSIS_NOT_FOUND error code, and makes spring.profiles.active environment-driven with dev default.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PresignController as JobPostingUploadController
  participant StorageService as JobPostingImageStorageService
  participant S3ObjectUrlService
  participant S3
  Client->>PresignController: POST /api/job-postings/images/presign-upload
  PresignController->>StorageService: createUploadPresignUrl(userId, request)
  StorageService->>S3ObjectUrlService: createPresignedPutUrl(key, contentType, minutes)
  S3ObjectUrlService->>S3: GeneratePresignedPutUrl
  S3-->>S3ObjectUrlService: presigned URL
  S3ObjectUrlService-->>StorageService: URL string
  StorageService-->>PresignController: JobPostingImageUploadPresignResponse
  PresignController-->>Client: ApiResponse with objectKey, uploadUrl, expiresInMinutes
  Client->>S3: PUT file to presigned URL
  S3-->>Client: 200 OK
Loading
sequenceDiagram
  participant Client
  participant ExtractController as JobPostingAiController
  participant IngestService as JobPostingIngestService
  participant AsyncFacade as JobPostingAsyncFacadeService
  participant AiService as JobPostingAiService
  participant StorageService as JobPostingImageStorageService
  participant OpenAI
  Client->>ExtractController: POST /api/job-postings/extract (rawText + imageObjectKey)
  ExtractController->>AiService: extractJobPosting(userId, rawText, imageObjectKey)
  AiService->>StorageService: createReadableImageUrl(userId, objectKey)
  StorageService-->>AiService: presigned GET URL
  AiService->>OpenAI: Extract with text + image URL
  OpenAI-->>AiService: Job posting data
  AiService-->>ExtractController: JobPostingExtractResponse
  ExtractController-->>Client: ApiResponse with extracted fields
  Client->>IngestService: POST /api/job-postings/ingest (rawText + imageObjectKey)
  IngestService->>AiService: extractJobPosting(userId, rawText, imageObjectKey)
  AiService->>OpenAI: Extract with text + image URL
  OpenAI-->>AiService: Job posting data
  AiService-->>IngestService: extracted response
  IngestService->>AsyncFacade: submit(user, JobPostingIngestRequest)
  AsyncFacade-->>Client: ApiResponse with jobId
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I dug a presign, neat and spry,
A key for uploads soaring high,
No multipart fuss, just PUT and cheer,
Images land where buckets hear,
And rabbits clap for analyses clear!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description follows the repository template structure with checkbox options and issue reference (#63), but the 'Detailed content' section is entirely empty with no explanation of the changes, rationale, or work performed. Fill in the '📋 세부 내용' section with a clear explanation of why the refactoring was necessary, what was changed (S3 presigned URLs, multipart-to-JSON migration, etc.), and any relevant implementation details or considerations.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.68% 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 The title 'Refactor/#63 s3 presigned' clearly refers to the main change: refactoring S3 image handling to use presigned URLs, as evidenced by extensive S3 configuration, presigner service, and S3 upload controller additions throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#63-s3-presigned

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a 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

🤖 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 `@build.gradle`:
- Around line 52-53: The build.gradle currently pins
io.awspring.cloud:spring-cloud-aws-starter-s3 to 3.1.1 which is incompatible
with Spring Boot 3.5.14; update the dependency to the 3.4.x release train
(replace version 3.1.1 with a 3.4.x version) and preferably manage Spring Cloud
AWS artifacts via the spring-cloud-aws-dependencies BOM by adding the BOM to
dependencyManagement and removing hardcoded versions so that
io.awspring.cloud:spring-cloud-aws-starter-s3 and related AWS starters are
resolved consistently for Spring Boot 3.5.14.

In `@ops/s3/job-posting-image-lifecycle.json`:
- Around line 7-10: The lifecycle policy currently expires every object with
Prefix "job-postings/" after 1 day which will delete all images because
JobPostingImageStorageService stores uploads under
job-postings/{userId}/{UUID}.{ext} (see buildObjectKey()) and
createReadableImageUrl() only presigns GETs and does not move files; either
narrow the lifecycle to a true temporary prefix (e.g., change the policy Prefix
to "job-postings/tmp/") or modify JobPostingImageStorageService.buildObjectKey()
to place temporary uploads under "job-postings/tmp/" (and ensure any
promotion/copy flow moves promoted files out of the tmp prefix), so permanent
images are not swept by the 1-day expiration.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingImageStorageService.java`:
- Around line 75-78: In createReadableImageUrl, check for a blank objectKey
before performing any S3 lookups: at the top of the method (before
validateOwnership and validateUploadedObject) return null (or throw the
INVALID_PARAMETER error used by your service) when objectKey is null/empty/blank
(e.g. if (objectKey == null || objectKey.trim().isEmpty()) return null;). Apply
the same early-blank-key guard to the other methods that call
validateUploadedObject/validateOwnership (the blocks referenced around lines
81-84 and 107-113) to avoid invoking s3ObjectUrlService.createPresignedGetUrl or
validateUploadedObject with a blank key.
- Around line 107-113: The call to s3Client.headObject in
validateUploadedObject(String objectKey) can throw S3 SDK exceptions that
currently bubble up as 500; wrap the headObject(...) invocation in a try/catch,
catch software.amazon.awssdk.services.s3.model.NoSuchKey / S3Exception (or the
broad S3Exception) and translate S3 404 to a domain NotFound/ClientError (e.g.,
throw a custom JobPostingImageNotFoundException or BadRequest/NotFound-specific
runtime exception), translate 403 to a Forbidden domain exception, and rethrow
or wrap other S3Exceptions as a mapped domain/internal exception; keep the
bucket/key lookup using s3ObjectUrlService.getBucket() and preserve the method
validateUploadedObject signature.

In `@src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Config.java`:
- Around line 16-23: Remove the hard-coded AWS static credentials and switch to
the SDK's default credential chain: delete the accessKey and secretKey fields in
S3Config and stop constructing AwsBasicCredentials; in s3Client() and
s3Presigner() either omit setting credentialsProvider or set it to
DefaultCredentialsProvider.create(), and update the builder calls accordingly;
also remove the now-unused AwsBasicCredentials import. Ensure
S3Config.s3Client() and S3Config.s3Presigner() still set the region but rely on
the DefaultCredentialsProvider so IAM roles/environment variables/EC2 metadata
work.

In
`@src/main/java/com/jobdri/jobdri_api/global/config/s3/S3ObjectUrlService.java`:
- Around line 33-34: Validate presigned URL expiration minutes before calling
signatureDuration in S3ObjectUrlService: ensure presignGetExpirationMinutes and
expiresInMinutes are within the SigV4 bounds (1..10080 minutes); if out of
range, either clamp to the nearest valid value or throw a clear
IllegalArgumentException. Add the checks right before the calls that build the
Get/Put presigners (the code that uses getObjectRequest and putObjectRequest and
calls .signatureDuration(Duration.ofMinutes(...))) so invalid config/input
cannot reach AwsPresigner and cause 500s.

In `@src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Uploader.java`:
- Around line 36-41: The upload(File uploadFile, String dirName) method
currently only calls removeNewFile(uploadFile) after a successful putS3 call,
leaving temp files if putS3 throws; update upload to ensure
removeNewFile(uploadFile) is executed regardless of success by wrapping the
putS3 call in a try/finally (or try/catch/finally) so the temp file is always
deleted, rethrow or propagate the original exception from putS3 unchanged (or
return the upload URL on success), and keep references to the methods
upload(File,String), putS3(File,String) and removeNewFile(File) to locate the
change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef07c63c-5503-47b4-91d2-1a0f08001c7f

📥 Commits

Reviewing files that changed from the base of the PR and between 011da53 and 1d4281d.

📒 Files selected for processing (25)
  • build.gradle
  • ops/s3/README.md
  • ops/s3/job-posting-image-cors.json
  • ops/s3/job-posting-image-lifecycle.json
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingUploadController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingImageUploadPresignRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingImageUploadPresignResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingImageStorageService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java
  • src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Config.java
  • src/main/java/com/jobdri/jobdri_api/global/config/s3/S3ObjectUrlService.java
  • src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Uploader.java
  • src/main/resources/application-dev.yaml
  • src/main/resources/application-prod.yaml
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestServiceTest.java
  • src/test/resources/application-test.yaml
💤 Files with no reviewable changes (2)
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java

Comment thread build.gradle Outdated
Comment thread ops/s3/job-posting-image-lifecycle.json Outdated
Comment thread src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Config.java Outdated
Comment thread src/main/java/com/jobdri/jobdri_api/global/config/s3/S3ObjectUrlService.java Outdated
Comment thread src/main/java/com/jobdri/jobdri_api/global/config/s3/S3Uploader.java Outdated
whc9999 and others added 4 commits May 22, 2026 22:33
- 자소서 분석 실행 API 추가
- 자소서 분석 결과 조회 API 추가
- 저장된 문항 답변과 공고 정보를 기반으로 LLM 분석 프롬프트 구성
- LLM JSON 응답을 Analysis 및 QuestionAnalysis 엔티티로 저장
- 재분석 시 기존 분석 결과와 문항 분석 결과를 교체하도록 처리
- 분석 완료 시 MockApply 상태를 COMPLETED로 변경
- 원문에 존재하지 않는 분석 sentence는 저장하지 않도록 검증
- start/end index를 서버에서 answer 기준으로 계산
- 점수를 0~100 범위로 보정
- 분석 결과 응답 DTO 및 LLM 응답 DTO 추가
- 분석 결과 없음 예외 코드 추가
- 로컬 실행 기본 프로필을 dev로 설정하고 dev JWT 기본값 추가
- 자소서 분석 서비스 테스트 추가
- 첨삭 문장 상태 enum 추가
- 분석 LLM 응답에 status 필드 반영
- 문항 분석 엔티티와 응답 DTO에 status 추가
- status 값 소문자 응답 및 기본값 보정 처리
- 분석 서비스 테스트에 status 검증 추가
- 문항 후보 조회 응답에 questionId 필드 추가
- 기본 문항 후보에 고정 ID 부여
- 문항 후보 조회 테스트에 ID 검증 추가
@shinae1023 shinae1023 merged commit 6e8ca27 into main May 22, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat New feature or request ♻️ refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants