[Feat] 분석 비동기 작업 취소 및 진행 상태 응답 추가#178
Conversation
- JD 분석 비동기 작업 취소 API 추가 - 자소서 분석 비동기 작업 취소 API 추가 - async task 상태에 CANCELLED 추가 - 상태 조회 및 SSE 응답에 진행 단계, 진행률, 예상 잔여 시간 필드 추가 - 자소서 분석 취소 시 예약된 크레딧만 멱등 환불 처리 - 취소된 작업의 worker 결과 저장 및 완료 callback 차단 - async task 진행/취소 컬럼 schema.sql 보강 - 취소 흐름 관련 단위 테스트 추가
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthrough분석 및 채용 공고 비동기 작업에 Changes비동기 작업 취소 및 진행 상태
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller
participant AsyncFacadeService
participant AsyncTaskService
participant AsyncTask
participant SseService
Client->>Controller: 취소 API 요청
Controller->>AsyncFacadeService: 사용자 검증 및 cancel 호출
AsyncFacadeService->>AsyncTaskService: 작업 취소 요청
AsyncTaskService->>AsyncTask: requestCancel()
AsyncTaskService->>SseService: 커밋 후 취소 상태 발행
AsyncTaskService-->>Controller: 취소 응답 반환
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java (1)
157-169: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win두 엔티티의
markFailed종료 상태 가드가 서로 다릅니다여기서는
isTerminal()(SUCCEEDED/FAILED/CANCELLED)로 막지만,AnalysisAsyncTask.markFailed()는SUCCEEDED || CANCELLED만 검사해 FAILED 재기록을 허용합니다. 동일 개념의 상태 머신이 도메인별로 다르게 동작하면 워커 재시도/타임아웃 스윕 경로에서 실패 사유가 한쪽에서만 갱신되는 혼선이 생깁니다. 의도된 차이인지 확인하고, 아니라면 한쪽으로 통일하는 편이 좋습니다.🤖 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/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java` around lines 157 - 169, 두 엔티티의 실패 처리 상태 가드를 동일하게 통일하세요. JobPostingAsyncTask.markFailed()의 isTerminal() 동작을 기준으로 AnalysisAsyncTask.markFailed()에서도 SUCCEEDED, FAILED, CANCELLED 상태의 재기록을 모두 차단하도록 수정하고, 기존 비종료 상태의 실패 처리 흐름은 유지하세요.src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java (1)
83-99: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win취소된 작업에
markSuccess가 들어오면 성공 알림과 메트릭이 잘못 발생합니다
SUCCEEDED/FAILED만 분기하고CANCELLED는 걸러내지 않습니다. 엔티티의markSuccess는isTerminal()가드로 no-op이 되지만, 이후recordProcessingMetric(task, "succeeded"),publishAfterCommit,createSuccessNotificationSafely는 그대로 실행됩니다. 결과적으로 사용자는 취소한 작업에 대해 "채용 공고 작업이 완료되었습니다" 알림을 받고, succeeded 메트릭이 오염됩니다.
JobPostingWorkerBridgeService가 앞단에서rejectIfCancelled로 막고 있긴 하지만, 서비스 자체가 public API인 만큼 여기서도 방어해야 합니다.AnalysisAsyncTaskService.markSuccess(90-97)도 동일하게 상태 가드가 없습니다.🐛 제안 diff
if (task.getStatus() == TaskStatus.FAILED) { throw new GeneralException( GeneralErrorCode.INVALID_PARAMETER, "이미 실패 처리된 채용 공고 비동기 작업입니다. taskId=" + taskId ); } + if (task.getStatus() == TaskStatus.CANCELLED) { + throw new GeneralException( + GeneralErrorCode.INVALID_PARAMETER, + "취소된 채용 공고 비동기 작업입니다. taskId=" + taskId + ); + } task.markSuccess(serializeResult(result));🤖 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/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java` around lines 83 - 99, Update JobPostingAsyncTaskService.markSuccess to explicitly handle CANCELLED tasks before invoking markSuccess or any success side effects; reject the request using the established invalid-parameter error pattern, so recordProcessingMetric, publishAfterCommit, and createSuccessNotificationSafely are not called. Apply the same cancellation guard to AnalysisAsyncTaskService.markSuccess.
🤖 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/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.java`:
- Around line 195-200: Remove the redundant status check inside requestCancel()
in AnalysisAsyncTask.java at lines 195-200 by checking only cancelledAt == null;
apply the same simplification in JobPostingAsyncTask.java at lines 176-181.
Preserve the existing cancellation timestamp assignment and return behavior.
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java`:
- Around line 288-295: 취소 시 마지막 실제 진행 단계가 유지되도록 두 서비스의 indexOfStep을 수정하세요.
AnalysisAsyncTaskService의 indexOfStep과 JobPostingAsyncTaskService의 indexOfStep은
미지 코드에 -1을 반환하도록 변경하고, 각 서비스의 buildSteps 및 resolveStepStatus 호출부가 취소된
currentStep을 마지막 유효 단계 기준으로 처리하도록 조정하세요. 엔티티의 취소 상태와 currentStep을 분리해 취소 직전 단계를
보존하세요.
- Around line 41-49: Extract the duplicated progress calculation logic from
AnalysisAsyncTaskService and JobPostingAsyncTaskService into a shared component
such as AsyncProgressCalculator, parameterized by progress steps and default
estimated remaining seconds. Move resolveCurrentStep, resolveProgressPercent,
resolveEstimatedRemainingSeconds, buildSteps, indexOfStep, resolveStepStatus,
and ProgressStepDefinition there; map each domain-specific TaskStatus enum to a
shared status representation, and update both services to delegate through the
component so fallback behavior is centralized.
- Around line 127-131: In the cancellation flow around requestCancel, invoke
task.requestCancel() before releaseCreditIfNeeded(task), and only release
credits when the cancellation transition actually succeeds. Preserve the
existing processing metric behavior for tasks whose status becomes CANCELLED,
while ensuring terminal SUCCEEDED or FAILED tasks are not refunded.
In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java`:
- Line 120: Rename the JobPostingAiController method
cancelIngestJobPostingAsyncStatus to cancelIngestJobPostingAsyncTask to reflect
that it cancels the async job itself, while preserving its implementation and
public URL contract.
In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.java`:
- Around line 59-72: 취소된 worker callback이 결과를 저장하거나 성공 처리하지 않는지 검증하는 테스트를 보강하세요.
src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.java#L59-L72에서는
취소된 작업에 대해 completeTask와 storeGeneratedResult가 실패하고 결과 upsert·도메인
저장·markSuccess가 호출되지 않는 테스트를 추가하세요.
src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeServiceTest.java#L86-L98에서는
completeTask, finalizeAndComplete, storeFinalizeResult 각각이 취소 후 upsert·공고 생성·성공
전이를 수행하지 않는지 검증하세요.
In
`@src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java`:
- Around line 263-279: Expand cancelTaskMarksCancelledAndPublishesStatus and
related JobPostingAsyncTaskServiceTest coverage to include unauthorized
cancellation asserting GeneralException when findByTaskIdAndUserId returns
empty, terminal SUCCEEDED/FAILED cancellation preserving status/message without
SSE publication, repeated cancellation preserving the original cancelledAt, and
RUNNING cancellation resetting progressPercent to 0 while setting completedAt
and cancelledAt.
---
Outside diff comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java`:
- Around line 157-169: 두 엔티티의 실패 처리 상태 가드를 동일하게 통일하세요.
JobPostingAsyncTask.markFailed()의 isTerminal() 동작을 기준으로
AnalysisAsyncTask.markFailed()에서도 SUCCEEDED, FAILED, CANCELLED 상태의 재기록을 모두 차단하도록
수정하고, 기존 비종료 상태의 실패 처리 흐름은 유지하세요.
In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java`:
- Around line 83-99: Update JobPostingAsyncTaskService.markSuccess to explicitly
handle CANCELLED tasks before invoking markSuccess or any success side effects;
reject the request using the established invalid-parameter error pattern, so
recordProcessingMetric, publishAfterCommit, and createSuccessNotificationSafely
are not called. Apply the same cancellation guard to
AnalysisAsyncTaskService.markSuccess.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 46ad38dd-124b-4acd-890c-4fae7cf073b1
📒 Files selected for processing (22)
src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisAsyncCancelResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisAsyncStatusResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisProgressStepResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSseService.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingAsyncCancelResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingAsyncStatusResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingProgressStepResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncSseService.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.javasrc/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.javasrc/main/resources/schema.sqlsrc/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.javasrc/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.javasrc/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeServiceTest.java
| private static final int DEFAULT_ESTIMATED_REMAINING_SECONDS = 180; | ||
| private static final List<ProgressStepDefinition> PROGRESS_STEPS = List.of( | ||
| new ProgressStepDefinition("VALIDATING_INPUT", "분석할 내용을 확인하고 있어요"), | ||
| new ProgressStepDefinition("PREPARING_CONTEXT", "공고와 자소서를 준비하고 있어요"), | ||
| new ProgressStepDefinition("CALLING_LLM", "자기소개서를 평가하고 있어요"), | ||
| new ProgressStepDefinition("VALIDATING_RESULT", "분석 결과를 검증하고 있어요"), | ||
| new ProgressStepDefinition("SAVING_RESULT", "분석 결과를 저장하고 있어요"), | ||
| new ProgressStepDefinition("COMPLETED", "분석이 완료되었습니다") | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
진행 단계 계산 로직이 두 서비스에 통째로 복제되어 있습니다
PROGRESS_STEPS 정의를 제외한 resolveCurrentStep / resolveProgressPercent / resolveEstimatedRemainingSeconds / buildSteps / indexOfStep / resolveStepStatus / ProgressStepDefinition이 JobPostingAsyncTaskService(274-347, 449-451)와 문자 그대로 동일합니다. 상태 판정 규칙이 한쪽에서만 바뀌면 두 API의 진행률 계약이 조용히 어긋납니다.
단계 목록과 기본 ETA만 파라미터로 받는 공용 컴포넌트(예: AsyncProgressCalculator)로 추출하고, 도메인별 TaskStatus enum은 공용 상태 열거로 매핑해 넘기는 구조를 권합니다. 위에서 지적한 indexOfStep fallback 수정도 한 곳에서 끝납니다.
Also applies to: 275-308, 380-382
🤖 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/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java`
around lines 41 - 49, Extract the duplicated progress calculation logic from
AnalysisAsyncTaskService and JobPostingAsyncTaskService into a shared component
such as AsyncProgressCalculator, parameterized by progress steps and default
estimated remaining seconds. Move resolveCurrentStep, resolveProgressPercent,
resolveEstimatedRemainingSeconds, buildSteps, indexOfStep, resolveStepStatus,
and ProgressStepDefinition there; map each domain-specific TaskStatus enum to a
shared status representation, and update both services to delegate through the
component so fallback behavior is centralized.
- 취소 시 currentStep을 CANCELLED로 덮지 않고 직전 진행 단계 유지 - async 진행 상태 계산 로직을 공통 AsyncProgressCalculator로 분리 - 취소 전이 성공 후에만 예약 크레딧을 환불하도록 처리 순서 수정 - 완료/실패된 task 취소 요청은 상태 유지 및 SSE 미발행 처리 - 취소된 task의 성공 전이와 worker 결과 저장 차단 - AnalysisAsyncTask 실패 처리 terminal guard를 JobPostingAsyncTask와 통일 - JD 분석 취소 컨트롤러 메서드명 정리 - 취소 상태 worker callback 및 task 취소 케이스 테스트 보강
✨ 어떤 이유로 PR를 하셨나요?
📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요
📸 작업 화면 스크린샷
🚨 관련 이슈 번호 [#177 ]
Summary by CodeRabbit