Skip to content

[✨Feat] 최종 작성본 일괄 저장 API 구현#53

Merged
yong203 merged 2 commits into
devfrom
feat/#52
Jul 1, 2026
Merged

[✨Feat] 최종 작성본 일괄 저장 API 구현#53
yong203 merged 2 commits into
devfrom
feat/#52

Conversation

@yong203

@yong203 yong203 commented Jul 1, 2026

Copy link
Copy Markdown
Member

작업 내용

  • 최종 작성본 일괄 저장 API(API-019)를 추가합니다.
  • 현재 사용자 소유 active 자기소개서와 해당 첨삭 버전 소속 여부를 검증합니다.
  • 최신 ReviewVersion이 아니면 REVIEW_VERSION_NOT_LATEST로 응답합니다.
  • 모든 문항의 finalAnswer를 전체 payload로 검증한 뒤 한 번에 저장합니다.
  • finalAnswer trim, blank, 5000자 초과, 누락/중복/알 수 없는 questionResultId validation을 처리합니다.
  • finalAnswerLength를 Unicode code point 기준으로 함께 갱신합니다.
  • controller/service 테스트를 추가하고 API/status 문서를 갱신합니다.

관련 이슈

문서 반영

  • 반영한 문서:
    • docs/api/README.md
    • docs/status.md

확인 결과

  • ./gradlew test
  • ./gradlew check
  • 기타: git diff --check
  • 실행하지 못함. 이유:

Summary by CodeRabbit

  • New Features
    • 최종 답변을 일괄 저장하는 엔드포인트가 추가되었습니다.
    • 저장 후 질문별 최종 답변/코드포인트 길이와 수정 시각이 함께 반환됩니다.
  • Bug Fixes
    • 최신 첨삭 버전이 아닌 경우 409로 제한되며, 명확한 오류가 제공됩니다.
    • 공백 답변, 누락/중복 항목, 형식/길이 규칙 위반 시 400으로 검증 오류를 표시합니다.
  • Documentation
    • API 상태 및 기능 진행 현황 문서가 최신 구현 내용으로 갱신되었으며, REQ-006 범위가 반영되었습니다.

최신 첨삭 버전의 문항별 최종 작성본을 전체 검증 후 저장하고, API-019 구현 상태와 REQ-006 진행 문서를 갱신한다.
@yong203 yong203 linked an issue Jul 1, 2026 that may be closed by this pull request
5 tasks
@yong203 yong203 self-assigned this Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

최신 첨삭 버전에 대한 문항별 최종 작성본 일괄 저장 PUT API가 추가되었다. 관련 예외 코드, 서비스 검증/저장 로직, DTO, 컨트롤러, 테스트, 문서가 함께 변경되었다.

Changes

최종 작성본 일괄 저장 기능

Layer / File(s) Summary
에러코드와 저장 타입
src/main/java/com/daon/rewrite/global/exception/ErrorCode.java, src/main/java/com/daon/rewrite/reviewversion/service/SaveFinalAnswerInput.java, src/main/java/com/daon/rewrite/reviewversion/service/SaveFinalAnswersResult.java
REVIEW_VERSION_NOT_LATEST 예외 코드와 저장 입력/결과 record가 추가되었다.
엔티티 최종답변 갱신
src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java, src/test/java/com/daon/rewrite/reviewversion/repository/ReviewVersionRepositoryTest.java
updateFinalAnswer가 finalAnswer와 finalAnswerLength를 갱신하고, 영속화 동작이 테스트로 확인된다.
커맨드 서비스 검증과 저장
src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java
최신 버전 검증, 입력 검증/정규화, 문항 결과 갱신, 결과 반환 흐름이 추가되었다.
요청/응답 DTO와 PUT 엔드포인트
src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersRequest.java, src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersResponse.java, src/main/java/com/daon/rewrite/reviewversion/controller/ReviewVersionController.java
요청/응답 변환과 PUT /cover-letters/{coverLetterId}/review-versions/{versionId}/final-answers 엔드포인트가 추가되었다.
컨트롤러와 서비스 테스트
src/test/java/com/daon/rewrite/reviewversion/controller/ReviewVersionControllerTest.java, src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java
성공, 최신 버전 아님, 검증 실패, NOT_FOUND 시나리오가 추가되었다.
문서 업데이트
docs/api/README.md, docs/status.md
API-019 상태와 REQ-006, 다음 후보 이슈 설명이 갱신되었다.

Possibly related PRs

  • Rewrite-Team/Rewrite-BE#43: ReviewVersionQuestionResult 모델을 도입한 PR로, 이번 변경의 updateFinalAnswer와 길이 갱신이 같은 엔티티를 확장합니다.
  • Rewrite-Team/Rewrite-BE#51: ReviewVersionController의 조회 API를 추가한 PR로, 이번 변경의 PUT 엔드포인트가 같은 컨트롤러에 붙습니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 최종 작성본 일괄 저장 API 추가라는 핵심 변경을 정확하고 간결하게 요약합니다.
Description check ✅ Passed 작업 내용, 관련 이슈, 문서 반영, 확인 결과가 템플릿대로 채워져 있어 설명이 충분합니다.
Linked Issues check ✅ Passed 최신 ReviewVersion 저장, 검증 오류 처리, NOT_FOUND 응답, 테스트와 문서 갱신이 요구사항과 일치합니다.
Out of Scope Changes check ✅ Passed 설명된 변경은 API-019 구현과 관련 문서·테스트 보강에 한정되어 있어 뚜렷한 범위 이탈이 보이지 않습니다.

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

@yong203 yong203 added the ✨ Feature 새로운 기능 구현 label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Backend CI Report

Item Result
Tests ✅ Passed
JaCoCo line coverage 95.79% (841/878 lines)
Coverage report ✅ Generated
Codecov upload ✅ Uploaded

View workflow run | View coverage details

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (3)
src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java (1)

108-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

countCodePoints 중복 정의.

동일한 코드포인트 계산 로직이 ReviewVersionCommandService.countCodePoints(184-186행)에도 존재합니다. 두 구현이 향후 divergence 되면 저장된 finalAnswerLength와 검증 시 사용하는 길이 기준이 어긋날 수 있습니다. 공용 유틸리티로 추출하는 것을 권장합니다.

🤖 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/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java`
around lines 108 - 115, `ReviewVersionQuestionResult.countCodePoints` duplicates
the same code-point length logic already present in
`ReviewVersionCommandService.countCodePoints`, so the two can drift apart.
Extract this into a shared utility or common helper and have both
`updateFinalAnswer` and `ReviewVersionCommandService` call that single
implementation to keep `finalAnswerLength` and validation length consistent.
src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java (1)

155-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

첨삭 버전(reviewVersion) NOT_FOUND 케이스 테스트 누락.

현재 saveMyFinalAnswersThrowsNotFoundWhenCoverLetterIsMissingOtherOwnerOrDeleted는 자기소개서(coverLetter) 기준의 NOT_FOUND만 검증합니다. PR 인수 기준은 "존재하지 않는 자기소개서/첨삭 버전" 모두에 대해 NOT_FOUND를 요구하지만, 존재하는 자기소개서에 대해 잘못된/존재하지 않는 versionId를 전달하는 케이스에 대한 테스트가 없습니다.

As per path instructions, **/*Test.java: "Add or update tests for new or changed behavior."

🤖 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/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java`
around lines 155 - 177, The current
`ReviewVersionCommandServiceTest.saveMyFinalAnswersThrowsNotFoundWhenCoverLetterIsMissingOtherOwnerOrDeleted`
only covers missing/unauthorized/deleted cover letters, but it does not verify
the NOT_FOUND behavior for a valid cover letter paired with an invalid or
nonexistent review version. Add a test in `ReviewVersionCommandServiceTest` that
saves an accessible cover letter, then calls `service.saveMyFinalAnswers` with a
bad `versionId` and asserts `BusinessException` with `ErrorCode.NOT_FOUND`,
using the existing `assertNotFound` pattern or a dedicated helper if needed.

Source: Path instructions

src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersResponse.java (1)

16-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

시간대 변환 로직 중복 가능성.

API_ZONE을 이용한 Instant→LocalDateTime 변환이 이미 다른 응답 DTO(예: 첨삭 버전 상세 응답의 createdAt 변환)에서도 동일하게 사용되는 것으로 보입니다. 새 DTO에서 동일한 상수/로직을 다시 정의하면 향후 시간대 정책이 바뀔 때 일부 DTO만 갱신되는 불일치 위험이 있습니다. 공통 유틸리티(예: TimeZoneUtils 또는 매핑 헬퍼)로 추출하는 것을 권장합니다.

♻️ 제안 예시
-public record SaveFinalAnswersResponse(
-        String coverLetterId,
-        String reviewVersionId,
-        List<QuestionResultResponse> questionResults,
-        LocalDateTime updatedAt
-) {
-    private static final ZoneId API_ZONE = ZoneId.of("Asia/Seoul");
-
-    public static SaveFinalAnswersResponse from(SaveFinalAnswersResult result) {
+public record SaveFinalAnswersResponse(
+        String coverLetterId,
+        String reviewVersionId,
+        List<QuestionResultResponse> questionResults,
+        LocalDateTime updatedAt
+) {
+    public static SaveFinalAnswersResponse from(SaveFinalAnswersResult result) {
         return new SaveFinalAnswersResponse(
                 result.coverLetterId(),
                 result.reviewVersionId(),
                 result.questionResults().stream()
                         .map(QuestionResultResponse::from)
                         .toList(),
-                LocalDateTime.ofInstant(result.updatedAt(), API_ZONE)
+                ApiTimeConverter.toLocalDateTime(result.updatedAt())
         );
     }
🤖 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/daon/rewrite/reviewversion/dto/SaveFinalAnswersResponse.java`
around lines 16 - 27, `SaveFinalAnswersResponse.from` is duplicating the
Instant-to-LocalDateTime conversion policy with its own `API_ZONE` constant,
which can drift from other DTO mappings. Extract this conversion into the shared
time-mapping utility or existing helper used by other response DTOs (for example
the same place that handles `createdAt`), and have
`from(SaveFinalAnswersResult)` delegate to that shared method instead of
defining its own zone/logic.
🤖 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/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java`:
- Around line 89-152: The duplicate `questionResultId` check in
`ReviewVersionCommandService` is tied to `normalizedAnswers`, so a repeated ID
can be missed when the first entry has a null/blank `finalAnswer`. Update the
validation flow to track already-seen IDs separately in
`validateQuestionResultId` (or the surrounding validation method) using a
dedicated Set, and use that Set for duplicate detection regardless of answer
validity. Keep the existing `normalizedAnswers` map only for collecting valid
answers, and ensure the duplicate error is added for the second occurrence even
if the first one failed answer validation.

---

Nitpick comments:
In
`@src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersResponse.java`:
- Around line 16-27: `SaveFinalAnswersResponse.from` is duplicating the
Instant-to-LocalDateTime conversion policy with its own `API_ZONE` constant,
which can drift from other DTO mappings. Extract this conversion into the shared
time-mapping utility or existing helper used by other response DTOs (for example
the same place that handles `createdAt`), and have
`from(SaveFinalAnswersResult)` delegate to that shared method instead of
defining its own zone/logic.

In
`@src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java`:
- Around line 108-115: `ReviewVersionQuestionResult.countCodePoints` duplicates
the same code-point length logic already present in
`ReviewVersionCommandService.countCodePoints`, so the two can drift apart.
Extract this into a shared utility or common helper and have both
`updateFinalAnswer` and `ReviewVersionCommandService` call that single
implementation to keep `finalAnswerLength` and validation length consistent.

In
`@src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java`:
- Around line 155-177: The current
`ReviewVersionCommandServiceTest.saveMyFinalAnswersThrowsNotFoundWhenCoverLetterIsMissingOtherOwnerOrDeleted`
only covers missing/unauthorized/deleted cover letters, but it does not verify
the NOT_FOUND behavior for a valid cover letter paired with an invalid or
nonexistent review version. Add a test in `ReviewVersionCommandServiceTest` that
saves an accessible cover letter, then calls `service.saveMyFinalAnswers` with a
bad `versionId` and asserts `BusinessException` with `ErrorCode.NOT_FOUND`,
using the existing `assertNotFound` pattern or a dedicated helper if needed.
🪄 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: accf64c8-3e0a-4e18-8dfc-4fb2b25cf500

📥 Commits

Reviewing files that changed from the base of the PR and between df48fdc and 982093c.

📒 Files selected for processing (12)
  • docs/api/README.md
  • docs/status.md
  • src/main/java/com/daon/rewrite/global/exception/ErrorCode.java
  • src/main/java/com/daon/rewrite/reviewversion/controller/ReviewVersionController.java
  • src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersRequest.java
  • src/main/java/com/daon/rewrite/reviewversion/dto/SaveFinalAnswersResponse.java
  • src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java
  • src/main/java/com/daon/rewrite/reviewversion/service/SaveFinalAnswerInput.java
  • src/main/java/com/daon/rewrite/reviewversion/service/SaveFinalAnswersResult.java
  • src/test/java/com/daon/rewrite/reviewversion/controller/ReviewVersionControllerTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java

중복 questionResultId 감지를 답변 유효성과 분리하고, 첨삭 버전 NOT_FOUND 및 finalAnswer 갱신 저장 테스트를 보강한다.

@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.

🧹 Nitpick comments (1)
src/test/java/com/daon/rewrite/reviewversion/repository/ReviewVersionRepositoryTest.java (1)

129-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

픽스처 생성 코드 중복.

saveFirstReviewVersionRoundTripsQuestionSnapshots(41-63행)와 동일한 CoverLetter/CoverLetterQuestion/ReviewVersion 생성 로직이 반복되고 있습니다. 공통 private 헬퍼 메서드로 추출하면 테스트 가독성과 유지보수성이 개선됩니다. 다만 필수는 아니며 우선순위는 낮습니다.

🤖 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/daon/rewrite/reviewversion/repository/ReviewVersionRepositoryTest.java`
around lines 129 - 153, The test setup in ReviewVersionRepositoryTest repeats
the same CoverLetter, CoverLetterQuestion, and ReviewVersion fixture creation
that already exists in saveFirstReviewVersionRoundTripsQuestionSnapshots.
Extract the shared setup into a private helper method (or a small fixture
builder) and reuse it from updateFinalAnswerRoundTripsUpdatedAnswerAndLength and
the existing test, keeping the assertions unchanged. Use the existing
create/save calls on CoverLetter.draft, CoverLetterQuestion.create, and
ReviewVersion.first so the duplicated setup is centralized and easier to
maintain.
🤖 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/daon/rewrite/reviewversion/repository/ReviewVersionRepositoryTest.java`:
- Around line 129-153: The test setup in ReviewVersionRepositoryTest repeats the
same CoverLetter, CoverLetterQuestion, and ReviewVersion fixture creation that
already exists in saveFirstReviewVersionRoundTripsQuestionSnapshots. Extract the
shared setup into a private helper method (or a small fixture builder) and reuse
it from updateFinalAnswerRoundTripsUpdatedAnswerAndLength and the existing test,
keeping the assertions unchanged. Use the existing create/save calls on
CoverLetter.draft, CoverLetterQuestion.create, and ReviewVersion.first so the
duplicated setup is centralized and easier to maintain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b535531-b880-47e2-a32f-fe92efa0b68e

📥 Commits

Reviewing files that changed from the base of the PR and between 982093c and 88421f1.

📒 Files selected for processing (3)
  • src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java
  • src/test/java/com/daon/rewrite/reviewversion/repository/ReviewVersionRepositoryTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java

@yong203 yong203 merged commit 019a66b into dev Jul 1, 2026
5 checks passed
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] 최종 작성본 일괄 저장 API 구현

1 participant