Skip to content

[✨Feat] AI 첨삭 다시받기 API 구현#55

Merged
yong203 merged 1 commit into
devfrom
feat/#54
Jul 3, 2026
Merged

[✨Feat] AI 첨삭 다시받기 API 구현#55
yong203 merged 1 commit into
devfrom
feat/#54

Conversation

@yong203

@yong203 yong203 commented Jul 3, 2026

Copy link
Copy Markdown
Member

작업 내용

  • POST /cover-letters/{coverLetterId}/review-versions AI 첨삭 다시받기 API를 추가했습니다.
  • 재첨삭 요청 시 현재 사용자 소유의 active 자기소개서인지 확인하고, REVIEWED 상태와 최신 첨삭 버전 존재 여부를 검증합니다.
  • 같은 자기소개서에 PENDING 또는 PROCESSING LLM Job이 있으면 LLM_JOB_ALREADY_RUNNING으로 막습니다.
  • llm_jobs.request_instruction을 추가해 재첨삭 요구사항을 Job에 저장하고, 완료된 새 ReviewVersion.requestInstruction으로 이어지게 했습니다.
  • 재첨삭 worker를 추가해 최신 ReviewVersion의 문항별 finalAnswer를 새 첨삭 입력 원본으로 사용합니다.
  • Job 이벤트 listener를 Job type별 dispatcher로 변경해 최초 첨삭과 재첨삭 worker가 분리되도록 했습니다.
  • 재첨삭 성공 시 v0.2, v0.3처럼 새 ReviewVersion을 생성하고 CoverLetter.latestReviewVersionId를 갱신합니다.
  • 재첨삭 실패 시 기존 최신 버전과 CoverLetter.status=REVIEWED는 유지하고 Job만 FAILED로 저장합니다.
  • API 상태, ERD, 진행 상태 문서를 갱신했습니다.

관련 이슈

문서 반영

  • 반영한 문서:
    • docs/api/README.md
    • docs/api/review-versions.md
    • docs/api/llm-jobs.md
    • docs/erd.md
    • docs/status.md

확인 결과

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

Summary by CodeRabbit

  • New Features

    • 재첨삭(다시 첨삭) 요청 기능이 추가되어, 사용자가 자기소개서에 대한 추가 수정 요청을 보낼 수 있습니다.
    • 재첨삭 진행 상태 조회 및 응답 정보가 확장되었습니다.
  • Bug Fixes

    • 재첨삭 요청 시 중복 실행을 방지하고, 실패 시 기존 결과를 안전하게 유지하도록 개선했습니다.
    • 요청 지시문 길이 검증과 상태 조건 검사가 강화되었습니다.
  • Documentation

    • API 문서와 진행 현황이 최신 동작에 맞게 업데이트되었습니다.

@yong203 yong203 linked an issue Jul 3, 2026 that may be closed by this pull request
8 tasks
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

자기소개서 최신 첨삭 버전의 최종 작성본을 기준으로 AI 첨삭을 다시 요청하는 재첨삭(Re-Review) 기능이 추가되었다. LlmJobrequestInstruction 필드가 추가되고, 재첨삭 요청 API, 트랜잭션 서비스, Worker, 이벤트 디스패치 로직, 새 ReviewVersion 생성 로직 및 관련 문서/테스트가 구현되었다.

Changes

재첨삭 기능 구현

Layer / File(s) Summary
LlmJob/ReviewVersion 엔티티 확장
LlmJob.java, ReviewVersion.java, ReviewVersionQuestionResult.java, LlmJobRepositoryTest.java
LlmJobrequestInstruction 컬럼/생성자 인자가 추가되고, ReviewVersion.reReview(...) 팩토리와 ReviewVersionQuestionResultoriginalAnswer 전달 방식이 변경됨.
재첨삭 요청 API
ReviewVersionController.java, RequestReReviewRequest.java, RequestReReviewResponse.java, RequestReReviewResult.java, ReviewVersionCommandService.java, ReviewVersionControllerTest.java, ReviewVersionCommandServiceTest.java
POST /cover-letters/{coverLetterId}/review-versions 엔드포인트, 요청/응답 DTO, 검증(상태/글자수/실행중 Job 충돌) 및 pending Job 생성·이벤트 발행 로직이 추가됨.
이벤트 디스패치 및 Worker 실행
FirstReviewJobEventListener.java, ReReviewJobTransactionService.java, ReReviewJobWorker.java, FirstReviewJobEventIntegrationTest.java, ReReviewJobWorkerTest.java
Job 타입에 따라 FirstReviewJobWorker/ReReviewJobWorker로 분기되고, 재첨삭 트랜잭션 시작/실패 처리와 실행 흐름이 구현됨.
재첨삭 완료 처리 및 프롬프트 반영
ReviewVersionService.java, FirstReviewRequest.java, OpenAiFirstReviewClient.java, OpenAiFirstReviewClientTest.java, ReviewVersionServiceTest.java
completeReReview가 최신 결과를 정규화해 새 ReviewVersion을 생성/저장하고, requestInstruction이 시스템/유저 프롬프트에 반영됨.
관련 문서 갱신
docs/api/README.md, docs/api/llm-jobs.md, docs/api/review-versions.md, docs/erd.md, docs/status.md
API 구현 상태, ERD 컬럼, 진행 현황 및 API 세부 규칙 문서가 재첨삭 기능에 맞춰 갱신됨.

Possibly related PRs

  • Rewrite-Team/Rewrite-BE#39: 동일한 LlmJob 엔티티를 확장하는 변경으로, 본 PR의 requestInstruction 필드 추가와 직접 연관됨.
  • Rewrite-Team/Rewrite-BE#43: ReviewVersion/ReviewVersionQuestionResult의 스냅샷 생성 로직을 확장하는 본 PR과 동일한 핵심 구조를 다룸.
  • Rewrite-Team/Rewrite-BE#45: OpenAiFirstReviewClientFirstReviewRequest 변경이 본 PR의 재첨삭 요구사항 프롬프트 반영과 직접 맞물림.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 주요 변경인 AI 첨삭 다시받기 API 구현을 간결하게 요약합니다.
Description check ✅ Passed 필수 섹션이 모두 채워져 있고 템플릿 구조도 잘 따릅니다.
Linked Issues check ✅ Passed 재첨삭 요청 검증, 중복 Job 차단, worker 분기, 최신 finalAnswer 기반 재생성, 실패 시 기존 버전 유지는 모두 반영되었습니다.
Out of Scope Changes check ✅ Passed 문서와 테스트 보강 외에 요구사항과 무관한 변경은 보이지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

@yong203 yong203 self-assigned this Jul 3, 2026
@yong203 yong203 added the ✨ Feature 새로운 기능 구현 label Jul 3, 2026
@yong203 yong203 marked this pull request as ready for review July 3, 2026 06:25
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Backend CI Report

Item Result
Tests ✅ Passed
JaCoCo line coverage 95.35% (1005/1054 lines)
Coverage report ✅ Generated
Codecov upload ✅ Uploaded

View workflow run | View coverage details

@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 (5)
docs/api/review-versions.md (1)

87-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validation 목록에 latestReviewVersionId 존재 조건 누락

ReviewVersionCommandService.requestMyReReviewstatus != REVIEWED || latestReviewVersionId == null 조건에서 모두 CONFLICT를 던지지만, 문서의 Validation 목록에는 REVIEWED 상태 조건만 기재되어 있습니다. PR 목표에도 두 조건이 함께 명시되어 있으므로 문서에도 반영하는 것이 좋습니다.

📝 제안
 requestInstruction: 선택, 최대 1000자
 같은 자기소개서에 PENDING 또는 PROCESSING 상태의 LLM Job이 없어야 한다.
 자기소개서 상태는 REVIEWED여야 한다.
+최신 첨삭 버전(latestReviewVersionId)이 존재해야 한다.
🤖 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 `@docs/api/review-versions.md` around lines 87 - 93, Validation 목록에
`latestReviewVersionId` 존재 조건이 빠져 있어
`ReviewVersionCommandService.requestMyReReview`의 실제 `CONFLICT` 조건과 문서가 불일치합니다.
`requestMyReReview`의 검증 기준을 정리하는 부분에서 `status == REVIEWED` 조건만 두지 말고
`latestReviewVersionId != null` 조건도 함께 명시하도록 `review-versions.md`의 Validation
항목을 수정하세요.
src/main/java/com/daon/rewrite/reviewversion/dto/RequestReReviewResponse.java (1)

14-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

coverLetterStatus를 하드코딩하지 말고 실제 값을 사용하세요.

현재는 requestMyReReview 진입 조건상 상태가 항상 REVIEWED이므로 결과적으로 맞지만, 실제 커버레터 엔티티 값을 조회하지 않고 상수를 응답에 박아두는 것은 향후 상태 전이 로직이 바뀔 경우(예: 재첨삭 진행 중 별도 상태 추가) 응답이 실제 상태와 어긋날 위험이 있습니다.

♻️ 제안 방향
-    public static RequestReReviewResponse from(RequestReReviewResult result) {
-        return new RequestReReviewResponse(
-                result.job().getId(),
-                result.coverLetterId(),
-                CoverLetterStatus.REVIEWED,
-                result.job().getStatus()
-        );
-    }
+    public static RequestReReviewResponse from(RequestReReviewResult result) {
+        return new RequestReReviewResponse(
+                result.job().getId(),
+                result.coverLetterId(),
+                result.coverLetterStatus(),
+                result.job().getStatus()
+        );
+    }

RequestReReviewResult에 실제 CoverLetter.getStatus() 값을 함께 담아 전달하도록 수정이 필요합니다.

🤖 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/RequestReReviewResponse.java`
around lines 14 - 21, `RequestReReviewResponse.from(RequestReReviewResult)` is
hardcoding `coverLetterStatus` to REVIEWED instead of using the actual entity
state. Update `RequestReReviewResult` to carry the real
`CoverLetter.getStatus()` value and have `from(...)` read that field when
building `RequestReReviewResponse`, so the response stays aligned with future
status changes.
src/main/java/com/daon/rewrite/reviewversion/service/ReReviewJobWorker.java (1)

11-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

FirstReviewJobWorker와 거의 동일한 구조의 중복 코드입니다.

transactionService.start → 외부 클라이언트 호출 → 성공 시 complete*, 실패 시 fail 처리하는 흐름이 (그래프 컨텍스트상) FirstReviewJobWorker.execute와 동일합니다. completeReReview/completeFirstReview 호출부만 파라미터화하면 공통 템플릿으로 추출할 수 있습니다. PR에서 의도적으로 워커를 분리했다는 점을 고려하면 우선순위는 낮습니다.

🤖 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/service/ReReviewJobWorker.java`
around lines 11 - 41, `ReReviewJobWorker.execute` duplicates the same
start/call/success/fail workflow already present in
`FirstReviewJobWorker.execute`; extract that shared template into a common
helper or base flow and parameterize only the completion step
(`completeReReview` vs `completeFirstReview`). Keep `transactionService.start`,
the external client call (`firstReviewClient.review`), and
`transactionService.fail` handling in the shared path, with only the
result-mapping and `reviewVersionService.completeReReview` remaining specific.
src/main/java/com/daon/rewrite/reviewversion/service/FirstReviewJobEventListener.java (1)

26-33: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

처리되지 않은 job 타입에 대한 로깅 추가를 고려하세요.

default 분기가 아무 동작 없이 무시되어, 향후 새 LlmJobType이 추가되고 분기 처리가 누락되면 해당 job은 아무 로그 없이 PENDING 상태로 영구히 방치될 수 있습니다.

♻️ 제안 diff
     private void dispatch(LlmJob job) {
         switch (job.getType()) {
             case COVER_LETTER_REVIEW -> firstReviewJobWorker.execute(job.getId());
             case COVER_LETTER_RE_REVIEW -> reReviewJobWorker.execute(job.getId());
-            default -> {
-            }
+            default -> log.warn("처리할 수 없는 LlmJobType입니다. jobId={}, type={}", job.getId(), job.getType());
         }
     }
🤖 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/service/FirstReviewJobEventListener.java`
around lines 26 - 33, Unhandled job types in
FirstReviewJobEventListener.dispatch are currently ignored, which can leave jobs
stuck without any visibility. Update the switch over job.getType() so the
default branch logs the unexpected LlmJobType and job id, using the dispatch
method and the existing worker calls (firstReviewJobWorker.execute and
reReviewJobWorker.execute) as the surrounding context. Keep the behavior
non-failing if that is intended, but make sure the log clearly identifies the
unsupported type so future missing LlmJobType cases are easy to detect.
src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionService.java (1)

252-255: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Count 기반 버전 넘버링은 데이터 삭제 시 충돌 위험이 있습니다.

countByCoverLetterId + 1 방식은 현재는 row-lock으로 동시성 문제는 방지되지만, 추후 ReviewVersion 행이 물리적으로 삭제되는 경로가 생기면(예: soft-delete cascade, 데이터 정리 배치 등) 카운트가 줄어들어 이미 사용된 버전 번호가 재사용될 수 있습니다. 기존 버전들 중 최대 patch 번호를 조회해 +1하는 방식이 더 안전합니다.

♻️ 제안: MAX 기반 버전 산정
-    private String nextVersion(String coverLetterId) {
-        long nextPatch = reviewVersionRepository.countByCoverLetterId(coverLetterId) + 1;
-        return "v0." + nextPatch;
-    }
+    private String nextVersion(String coverLetterId) {
+        long nextPatch = reviewVersionRepository.findMaxPatchByCoverLetterId(coverLetterId)
+                .orElse(0L) + 1;
+        return "v0." + nextPatch;
+    }
🤖 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/service/ReviewVersionService.java`
around lines 252 - 255, The current nextVersion method in ReviewVersionService
derives the patch number from countByCoverLetterId, which can reuse an old
version if ReviewVersion rows are ever removed. Change the version calculation
to use the maximum existing patch number for the given coverLetterId and
increment that value instead, so nextVersion always advances from the highest
used version rather than the row count.
🤖 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 `@docs/api/review-versions.md`:
- Around line 87-93: Validation 목록에 `latestReviewVersionId` 존재 조건이 빠져 있어
`ReviewVersionCommandService.requestMyReReview`의 실제 `CONFLICT` 조건과 문서가 불일치합니다.
`requestMyReReview`의 검증 기준을 정리하는 부분에서 `status == REVIEWED` 조건만 두지 말고
`latestReviewVersionId != null` 조건도 함께 명시하도록 `review-versions.md`의 Validation
항목을 수정하세요.

In
`@src/main/java/com/daon/rewrite/reviewversion/dto/RequestReReviewResponse.java`:
- Around line 14-21: `RequestReReviewResponse.from(RequestReReviewResult)` is
hardcoding `coverLetterStatus` to REVIEWED instead of using the actual entity
state. Update `RequestReReviewResult` to carry the real
`CoverLetter.getStatus()` value and have `from(...)` read that field when
building `RequestReReviewResponse`, so the response stays aligned with future
status changes.

In
`@src/main/java/com/daon/rewrite/reviewversion/service/FirstReviewJobEventListener.java`:
- Around line 26-33: Unhandled job types in FirstReviewJobEventListener.dispatch
are currently ignored, which can leave jobs stuck without any visibility. Update
the switch over job.getType() so the default branch logs the unexpected
LlmJobType and job id, using the dispatch method and the existing worker calls
(firstReviewJobWorker.execute and reReviewJobWorker.execute) as the surrounding
context. Keep the behavior non-failing if that is intended, but make sure the
log clearly identifies the unsupported type so future missing LlmJobType cases
are easy to detect.

In `@src/main/java/com/daon/rewrite/reviewversion/service/ReReviewJobWorker.java`:
- Around line 11-41: `ReReviewJobWorker.execute` duplicates the same
start/call/success/fail workflow already present in
`FirstReviewJobWorker.execute`; extract that shared template into a common
helper or base flow and parameterize only the completion step
(`completeReReview` vs `completeFirstReview`). Keep `transactionService.start`,
the external client call (`firstReviewClient.review`), and
`transactionService.fail` handling in the shared path, with only the
result-mapping and `reviewVersionService.completeReReview` remaining specific.

In
`@src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionService.java`:
- Around line 252-255: The current nextVersion method in ReviewVersionService
derives the patch number from countByCoverLetterId, which can reuse an old
version if ReviewVersion rows are ever removed. Change the version calculation
to use the maximum existing patch number for the given coverLetterId and
increment that value instead, so nextVersion always advances from the highest
used version rather than the row count.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b2efa216-7f71-4b72-b26f-b36f66c9167d

📥 Commits

Reviewing files that changed from the base of the PR and between 019a66b and 574e6e6.

📒 Files selected for processing (27)
  • docs/api/README.md
  • docs/api/llm-jobs.md
  • docs/api/review-versions.md
  • docs/erd.md
  • docs/status.md
  • src/main/java/com/daon/rewrite/llmjob/entity/LlmJob.java
  • src/main/java/com/daon/rewrite/reviewversion/client/FirstReviewRequest.java
  • src/main/java/com/daon/rewrite/reviewversion/client/OpenAiFirstReviewClient.java
  • src/main/java/com/daon/rewrite/reviewversion/controller/ReviewVersionController.java
  • src/main/java/com/daon/rewrite/reviewversion/dto/RequestReReviewRequest.java
  • src/main/java/com/daon/rewrite/reviewversion/dto/RequestReReviewResponse.java
  • src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersion.java
  • src/main/java/com/daon/rewrite/reviewversion/entity/ReviewVersionQuestionResult.java
  • src/main/java/com/daon/rewrite/reviewversion/repository/ReviewVersionRepository.java
  • src/main/java/com/daon/rewrite/reviewversion/service/FirstReviewJobEventListener.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReReviewJobTransactionService.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReReviewJobWorker.java
  • src/main/java/com/daon/rewrite/reviewversion/service/RequestReReviewResult.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandService.java
  • src/main/java/com/daon/rewrite/reviewversion/service/ReviewVersionService.java
  • src/test/java/com/daon/rewrite/llmjob/repository/LlmJobRepositoryTest.java
  • src/test/java/com/daon/rewrite/reviewversion/client/OpenAiFirstReviewClientTest.java
  • src/test/java/com/daon/rewrite/reviewversion/controller/ReviewVersionControllerTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/FirstReviewJobEventIntegrationTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/ReReviewJobWorkerTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionCommandServiceTest.java
  • src/test/java/com/daon/rewrite/reviewversion/service/ReviewVersionServiceTest.java

@yong203 yong203 merged commit 6a45b12 into dev Jul 3, 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] AI 첨삭 다시받기 API 및 worker 구현

1 participant