Skip to content

Feat/#109 polling to sse#120

Merged
shinae1023 merged 7 commits into
devfrom
feat/#109-polling-to-sse
Jul 7, 2026
Merged

Feat/#109 polling to sse#120
shinae1023 merged 7 commits into
devfrom
feat/#109-polling-to-sse

Conversation

@shinae1023

@shinae1023 shinae1023 commented Jul 7, 2026

Copy link
Copy Markdown
Member

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

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

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

기존 async 처리 구조는 채용공고와 자소서 분석이 모두 MQ 기반으로 동작하지만, 상태 전이와 복구 방식이 서로 달라 운영 일관성이 떨어졌습니다. 특히 채용공고 async는 retry 상태머신과 worker 메타데이터 반영이 부족했고, 프론트는 task 완료 여부를 폴링으로만 확인하고 있었습니다.

이번 PR에서는 다음 내용을 반영했습니다.

  1. 채용공고 async retry 상태머신 보강
  • 채용공고 async에도 retryCount, workerId, queueLatencyMillis 등 worker 메타데이터를 반영하도록 확장했습니다.
  • max retry 도달 시 자동으로 FAILED 상태로 전이되도록 보강했습니다.
  • worker internal API의 running / retry / failed 요청 계약을 analysis와 유사한 수준으로 정리했습니다.
  1. async 상태 SSE 스트림 추가
  • 채용공고와 자소서 분석 모두 polling 대신 SSE로 상태를 구독할 수 있는 스트림 endpoint를 추가했습니다.
  • async task 상태 변경 시점마다 SSE 이벤트를 발행하도록 연결했습니다.
  • terminal 상태(SUCCEEDED / FAILED)에서는 마지막 상태를 push한 뒤 스트림이 종료되도록 처리했습니다.
  1. SSE heartbeat 추가
  • 장시간 열려 있는 SSE 연결이 프록시나 브라우저에서 idle connection으로 끊기지 않도록 heartbeat 이벤트를 주기적으로 발행하도록 보강했습니다.
  1. stuck-task sweep 추가
  • 조회 요청이 없어도 백그라운드에서 PENDING / RUNNING task를 주기적으로 점검하도록 스케줄러를 추가했습니다.
  • timeout이 지난 task는 FAILED로 정리하고, 해당 상태 변경도 SSE로 즉시 전파되도록 했습니다.
  • analysis async는 timeout 실패 시 예약된 크레딧이 있으면 함께 환불되도록 처리했습니다.

기대 효과:

  • 프론트가 polling 없이 실시간 상태 변경을 받을 수 있습니다.
  • worker 장애나 유실로 인해 장시간 멈춘 task가 방치되지 않습니다.
  • 채용공고와 자소서 분석 async 처리 방식이 더 일관되게 정리됩니다.

📸 작업 화면 스크린샷

  • 백엔드 비동기 상태 처리 로직 변경으로 별도 UI 스크린샷은 없습니다.

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

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

🚨 관련 이슈 번호 [ #109 ]

채용공고 및 자소서 분석 async 상태 조회를 폴링 방식에서 SSE 기반 스트림 방식으로 확장했습니다. 공통 SSE emitter 레지스트리를 추가하고, async task 상태 변경 시점마다 즉시 이벤트를 발행하도록 연결해 프론트가 실시간으로 상태를 반영할 수 있게 했습니다.
SSE 기반 async 상태 스트림에 주기적인 heartbeat 이벤트를 추가해 장시간 유지되는 연결이 프록시나 브라우저에서 유휴 연결로 끊기지 않도록 보강했습니다. 공통 SSE 레지스트리에서 heartbeat를 관리하도록 구성해 채용공고 및 자소서 분석 스트림에 동일하게 적용했습니다.
비동기 task가 PENDING 또는 RUNNING 상태에서 장시간 멈춰 있을 때 주기적으로 timeout 여부를 점검해 FAILED로 정리하는 stuck-task sweep을 추가했습니다. 채용공고는 상태만 정리하고, 자소서 분석은 timeout 실패 시 예약된 크레딧을 함께 환불하도록 구성해 비동기 복구와 SSE 상태 전파의 운영 일관성을 높였습니다.
@shinae1023 shinae1023 self-assigned this Jul 7, 2026
@shinae1023 shinae1023 added ✨ feat New feature or request ♻️ refactor labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shinae1023, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e48bfddf-0986-48cb-8207-3686b474723e

📥 Commits

Reviewing files that changed from the base of the PR and between 0faa17a and 06df346.

📒 Files selected for processing (15)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSseService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSweepService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerFailureRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRetryRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncSseService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/global/scheduling/AsyncTaskSweepScheduler.java
  • src/main/java/com/jobdri/jobdri_api/global/sse/SseSubscriptionRegistry.java
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java
📝 Walkthrough

Walkthrough

This PR adds SSE-based streaming of async task status for analysis and job-posting domains via a shared SseSubscriptionRegistry, renames analysis credit handling from reserve/confirm/release to deduct/refund, adds sweep services for timed-out tasks with a scheduled trigger, tracks job-posting worker metadata, and introduces an UNKNOWN payment status for confirmation timeouts.

Changes

Async Task SSE Streaming and Sweeping

Layer / File(s) Summary
Shared SSE registry
global/sse/SseSubscriptionRegistry.java
New component managing per-channel SSE subscriptions, heartbeats, publish/subscribe, and cleanup on error/timeout.
Analysis SSE streaming
domain/analysis/service/AnalysisAsyncSseService.java, domain/analysis/controller/AnalysisController.java, domain/analysis/service/AnalysisAsyncTaskService.java
New streaming endpoint and service publish/subscribe status updates on analysis task transitions.
Analysis credit rename and sweep
domain/analysis/service/AnalysisService.java, .../AnalysisWorkerBridgeService.java, .../AnalysisAsyncSweepService.java, .../repository/AnalysisAsyncTaskRepository.java, test/.../analysis/service/*
reserveAnalysisCredit/confirmAnalysisCredit/releaseAnalysisCredit replaced by deductAnalysisCredit/refundAnalysisCredit; new sweep service expires stuck tasks and refunds credit; repository gains findByStatusIn; tests updated; descriptive comments added across analysis services.
Job-posting SSE streaming
domain/jobposting/service/JobPostingAsyncSseService.java, .../controller/JobPostingAiController.java, .../service/JobPostingAsyncTaskService.java
New streaming endpoint and service publish status updates on job-posting task transitions.
Job-posting worker metadata and sweep
domain/jobposting/entity/JobPostingAsyncTask.java, .../dto/worker/*, .../controller/JobPostingWorkerInternalController.java, .../service/JobPostingWorkerBridgeService.java, .../repository/JobPostingAsyncTaskRepository.java, test/.../jobposting/service/*
Adds workerId/queueLatencyMillis tracking, terminal-state guards, updateWorkerMetadata, task lookup helper, findByStatusIn, and sweepTimedOutTasks; tests added.
Scheduled sweep trigger
JobdriApiApplication.java, global/scheduling/AsyncTaskSweepScheduler.java
Enables scheduling and adds a periodic scheduler invoking both sweep services.

Payment Confirmation Timeout Handling

Layer / File(s) Summary
UNKNOWN payment status
domain/payment/entity/Payment.java, .../entity/PaymentStatus.java, .../service/PaymentService.java, .../service/PaymentTransactionService.java, test/.../payment/service/PaymentServiceTest.java
Adds PaymentStatus.UNKNOWN, replaces resetToPending() with markUnknown(), and renames rollbackConfirmationToPending to markConfirmationUnknown, used on external-service timeout.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Controller
  participant SseSubscriptionRegistry
  Client->>Controller: GET .../async/{taskId}/stream
  Controller->>SseSubscriptionRegistry: subscribe(channelKey, initialStatus)
  SseSubscriptionRegistry-->>Client: connected + status event
  Note over Controller,SseSubscriptionRegistry: On status transition
  Controller->>SseSubscriptionRegistry: publish(channelKey, updatedStatus)
  SseSubscriptionRegistry-->>Client: status update event
Loading
sequenceDiagram
  participant Scheduler
  participant AnalysisAsyncSweepService
  participant JobPostingAsyncTaskService
  participant AnalysisService
  Scheduler->>AnalysisAsyncSweepService: sweepTimedOutTasks()
  AnalysisAsyncSweepService->>AnalysisService: refundAnalysisCredit(user, referenceId)
  Scheduler->>JobPostingAsyncTaskService: sweepTimedOutTasks()
  JobPostingAsyncTaskService-->>Scheduler: expired task counts
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise and accurately reflects the main change from polling to SSE.
Description check ✅ Passed The description follows the template and covers motivation, details, screenshots, checklist, and issue number.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#109-polling-to-sse

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.

❤️ Share

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

@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: 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/service/JobPostingAsyncTaskService.java (1)

52-97: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

SSE publish() fires before the transaction commits.

In markRunning (Line 59), markSuccess (Line 75), markRetryScheduled (Line 86), markFailed (Line 96), and sweepTimedOutTasks (Line 133), jobPostingAsyncSseService.publish(...) is called on the in-memory, not-yet-committed entity state, before the @Transactional method returns and the transaction actually commits. This means a subscribed client can receive an SSE event (e.g. terminal SUCCEEDED/FAILED, which also closes their SSE stream) for a state that hasn't been durably persisted yet. If the flush/commit subsequently fails (constraint violation, connection issue, etc.), the client would have already been notified — and its stream closed — for a transition that never actually took effect, while a fresh poll of GET /ingest/async/{taskId} could show different data.

Consider publishing only after the transaction successfully commits, e.g. via TransactionSynchronizationManager.registerSynchronization(...)'s afterCommit() hook, so notifications are only ever sent for durably-committed state.

♻️ Sketch of a post-commit publish helper
private void publishAfterCommit(JobPostingAsyncStatusResponse response) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            `@Override`
            public void afterCommit() {
                jobPostingAsyncSseService.publish(response);
            }
        });
    } else {
        jobPostingAsyncSseService.publish(response);
    }
}

Replace each jobPostingAsyncSseService.publish(toStatusResponse(task)) call with publishAfterCommit(toStatusResponse(task)).

Also applies to: 126-138

🤖 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 52 - 97, `JobPostingAsyncTaskService` is publishing SSE updates
before the enclosing transaction commits, so clients can observe uncommitted or
later-rolled-back state. Update `markRunning`, `markSuccess`,
`markRetryScheduled`, `markFailed`, and `sweepTimedOutTasks` to publish via a
post-commit hook instead of calling
`jobPostingAsyncSseService.publish(toStatusResponse(task))` directly. Add a
helper in this service (or equivalent) that registers `afterCommit()` with
`TransactionSynchronizationManager` and only publishes the
`toStatusResponse(task)` result once the transaction has successfully committed.
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java (1)

72-88: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Move the credit reservation out of analyze()'s transaction.

deductAnalysisCredit() and refundAnalysisCredit() both run inside the outer analyze() transaction, so the credit change isn’t committed before executeAnalysis() starts. If the AI call or later finalize step throws, the refund is rolled back with the original deduction, and the DB work stays open for the whole remote request. Use an independent transaction for the reservation/refund so the balance change commits separately.

🤖 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/AnalysisService.java`
around lines 72 - 88, The credit reservation logic in AnalysisService.analyze()
is running inside the outer transaction, so deductAnalysisCredit() and
refundAnalysisCredit() are not committed independently before executeAnalysis()
starts. Move the reservation/refund handling into its own transactional boundary
(for example via a separate method on AnalysisService or a dedicated helper) so
the balance change commits separately, and keep analyze() focused on
validateAnalysisRequest(), prepareAnalysisExecution(), executeAnalysis(), and
finalizeAnalysis().
🧹 Nitpick comments (12)
src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java (1)

86-95: 🗄️ Data Integrity & Integration | 🔵 Trivial

No automated reconciliation path for stuck UNKNOWN payments.

markConfirmationUnknown only flips status to UNKNOWN; recovery relies entirely on the same user re-issuing a confirm request with the same paymentKey. If the user abandons the flow after a timeout (browser closed, app killed), the payment stays UNKNOWN indefinitely even if Toss actually completed the charge — resulting in a charged customer with no credited balance, discoverable only via manual investigation.

Toss exposes a payment inquiry API by paymentKey/orderId that could be polled by a scheduled reconciliation job (similar in spirit to the async-task sweep added elsewhere in this PR) to resolve UNKNOWN payments automatically.

🤖 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/payment/service/PaymentTransactionService.java`
around lines 86 - 95, The markConfirmationUnknown flow in
PaymentTransactionService only moves a payment to UNKNOWN and leaves no path to
recover stuck payments later. Update the payment lifecycle so UNKNOWN entries
can be reconciled automatically, ideally by adding a scheduled reconciliation
job that uses Toss inquiry by paymentKey/orderId and reuses the payment state
transitions in PaymentTransactionService. Make the fix centered around
markConfirmationUnknown and the existing payment status handling so abandoned
flows are eventually resolved without relying on the user to retry.
src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java (1)

186-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for UNKNOWN retry with a mismatched paymentKey.

Current tests cover the happy-path retry (same paymentKey after timeout → success), but not the branch in startConfirmation where an UNKNOWN payment receives a confirm request with a different paymentKey and should throw PAYMENT_ALREADY_PROCESSED. Adding this case would close coverage on the new branch.

🤖 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/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java`
around lines 186 - 221, Add a test to cover the UNKNOWN retry path when the
confirm request uses a different paymentKey than the stored payment. Extend
PaymentServiceTest around confirmAllowsRetryWhenTossConfirmTimesOut() to first
force a timeout, then call paymentService.confirm with a mismatched
PaymentConfirmRequest and assert GeneralException with
GeneralErrorCode.PAYMENT_ALREADY_PROCESSED. Use the existing
paymentService.confirm, TossPaymentConfirmResponse, and
paymentRepository.findByOrderId setup to exercise the startConfirmation branch
for UNKNOWN payments.
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSseService.java (1)

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

Terminal-status check via hardcoded strings duplicated across domains.

isTerminal compares statusResponse.status() against the literal strings "SUCCEEDED"/"FAILED", and the identical logic is duplicated verbatim in JobPostingAsyncSseService. If the underlying TaskStatus enum values ever change, these string literals won't get a compile-time signal. Consider exposing/using the domain TaskStatus enum (or a shared helper) instead of string literals, and/or extracting the shared subscribe/publish/isTerminal boilerplate into a small generic helper reused by both domains.

🤖 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/AnalysisAsyncSseService.java`
around lines 39 - 41, The terminal-status check in AnalysisAsyncSseService is
hardcoded with duplicated string literals, and the same logic exists in
JobPostingAsyncSseService. Replace the literal comparisons in isTerminal with
the shared domain TaskStatus enum (or a shared helper based on it) so status
checks are compile-time safe. If possible, extract the repeated
subscribe/publish/isTerminal flow into a small reusable helper used by both
services to remove the duplicated boilerplate.
src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java (1)

126-138: 🚀 Performance & Scalability | 🔵 Trivial

Consider batching the sweep query for scalability.

sweepTimedOutTasks() loads and processes every PENDING/RUNNING task in a single transaction/loop with no pagination. This is fine at current scale but could become a long-running transaction (and hold row locks) if the number of stuck tasks grows. Worth keeping in mind as usage scales — e.g. paginate via Pageable or cap the batch size per scheduler tick.

🤖 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 126 - 138, The sweep in
JobPostingAsyncTaskService.sweepTimedOutTasks() processes all PENDING/RUNNING
tasks in one transaction, which can become a long-running lock-heavy operation
as data grows. Update the repository call and loop to process tasks in bounded
batches, using a Pageable/limit or similar batch size per scheduler tick, and
keep the existing expireTimedOutTaskIfNeeded and publish(toStatusResponse(task))
flow unchanged for each processed task.
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java (1)

153-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Naming still implies a hold/reservation, though the underlying call now performs a direct deduction.

reserveCreditIfNeeded/markCreditReserved/CreditStatus.RESERVED/confirmCreditIfNeeded retain reserve/confirm terminology, but they now wrap analysisService.deductAnalysisCredit(...), which charges credit immediately rather than placing a hold. This is functionally fine (release/refund still checks RESERVED correctly), but the mismatch between naming and actual semantics could confuse future maintainers reasoning about credit lifecycle guarantees.

Consider renaming to align with the AnalysisService deduct/refund terminology (e.g., deductCreditIfNeeded/markCreditDeducted) for consistency.

🤖 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/AnalysisWorkerBridgeService.java`
around lines 153 - 169, The credit lifecycle names in
AnalysisWorkerBridgeService still imply a reservation/hold even though the flow
now performs an immediate deduction via
analysisService.deductAnalysisCredit(...). Update the helper names and related
status/marking methods to match the actual semantics (for example,
reserveCreditIfNeeded, markCreditReserved, and confirmCreditIfNeeded should
align with deduction/confirmation terminology), while keeping the existing
release/refund behavior consistent with CreditStatus and the task reference ID
handling.
src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java (2)

138-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM on this test itself.

Separately, consider adding a test for the new isTerminal() guard in markRetryScheduled (e.g., calling it on an already-SUCCEEDED/FAILED task should be a no-op), mirroring AnalysisWorkerBridgeServiceTest.terminalTaskDoesNotReopen.

🤖 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/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java`
around lines 138 - 149, The new isTerminal() guard in markRetryScheduled is not
covered by tests; add a test in JobPostingAsyncTaskServiceTest that calls
markRetryScheduled on an already terminal JobPostingAsyncTask (SUCCEEDED or
FAILED) and verifies it remains unchanged, similar to
AnalysisWorkerBridgeServiceTest.terminalTaskDoesNotReopen. Use the existing
markRetryScheduled and isTerminal behavior to keep the assertion focused on the
no-op path.

118-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider also asserting the SSE publish on this transition.

This test verifies the entity-level state transition but not that jobPostingAsyncSseService.publish(...) fires when a task is auto-failed via max-retry exhaustion — the core behavior this PR cohort introduces.

🤖 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/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java`
around lines 118 - 136, The max-retry failure test in
JobPostingAsyncTaskServiceTest only checks the entity state transition; update
it to also verify that JobPostingAsyncSseService.publish(...) is invoked when
markRetryScheduled(...) auto-transitions a task to FAILED. Use the existing task
setup and add a verification for the publish call on the max-retry exhaustion
path so the test covers the new SSE behavior introduced by
JobPostingAsyncTaskService.markRetryScheduled.
src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java (1)

126-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Inconsistent terminal-state guard between markFailed and markRetryScheduled.

markRetryScheduled uses isTerminal() (SUCCEEDED or FAILED) to no-op, but markFailed only guards against SUCCEEDED. A second markFailed(...) call on an already-FAILED task will still overwrite completedAt/error/failureReason/retryCount and can trigger a duplicate SSE publish downstream. Current callers happen to guard before invoking this, but the entity's own invariant should be self-consistent to avoid this becoming a bug when a new caller is added.

♻️ Proposed fix
     public void markFailed(FailureReason failureReason, String errorMessage, int retryCount) {
-        if (status == TaskStatus.SUCCEEDED) {
+        if (isTerminal()) {
             return;
         }
🤖 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 126 - 136, `JobPostingAsyncTask.markFailed` has a weaker
terminal-state check than `markRetryScheduled`, so a second failure can
overwrite an already-FAILED task. Update `markFailed(FailureReason, String,
int)` to use the same terminal guard as `isTerminal()` (or equivalent) so it
no-ops for both SUCCEEDED and FAILED states, keeping `completedAt`, `error`,
`failureReason`, and `retryCount` stable on repeat calls.
src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRetryRequest.java (1)

11-13: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add @Size(max = 100) to workerId to match the entity column length.

Same as the JobPostingWorkerFailureRequest DTO — JobPostingAsyncTask.workerId is capped at 100 chars, so it's worth enforcing that at validation time here too.

♻️ Proposed fix
         `@Min`(0) int retryCount,
-        `@NotBlank` String workerId,
+        `@NotBlank` `@Size`(max = 100) String workerId,
         Long queueLatencyMillis
🤖 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/dto/worker/JobPostingWorkerRetryRequest.java`
around lines 11 - 13, Add a size constraint to
JobPostingWorkerRetryRequest.workerId so it matches the 100-character limit used
by JobPostingAsyncTask and the similar JobPostingWorkerFailureRequest DTO.
Update the workerId field annotation in JobPostingWorkerRetryRequest to include
`@Size`(max = 100) alongside the existing non-blank validation, keeping validation
aligned with the entity column length.
src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerFailureRequest.java (1)

11-13: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add @Size(max = 100) to workerId to match the entity column length.

JobPostingAsyncTask.workerId is @Column(name = "worker_id", length = 100). Without a matching @Size constraint here, an oversized value fails at the DB layer (or gets silently truncated depending on DB config) instead of failing validation with a clear error.

♻️ Proposed fix
         `@Min`(0) int retryCount,
-        `@NotBlank` String workerId,
+        `@NotBlank` `@Size`(max = 100) String workerId,
         Long queueLatencyMillis
🤖 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/dto/worker/JobPostingWorkerFailureRequest.java`
around lines 11 - 13, The workerId field in JobPostingWorkerFailureRequest only
has `@NotBlank`, so oversized values can slip past validation and fail later
against the JobPostingAsyncTask.workerId column length. Update the
JobPostingWorkerFailureRequest record to add `@Size`(max = 100) on workerId
alongside the existing validation annotations so the DTO matches the entity
constraint and fails fast during validation.
src/main/java/com/jobdri/jobdri_api/global/scheduling/AsyncTaskSweepScheduler.java (1)

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

A throwing job-posting sweep permanently starves the analysis sweep each cycle.

jobPostingAsyncTaskService.sweepTimedOutTasks() and analysisAsyncSweepService.sweepTimedOutTasks() run sequentially with no error isolation. If the job-posting sweep throws (persistently, e.g. due to a malformed task or a bug), the analysis sweep never executes for that invocation — and since the same failure would recur every scheduled cycle, analysis tasks would never get swept until the underlying bug is fixed.

Wrap each call independently so a failure in one domain doesn't block the other:

🛡️ Suggested isolation
     public void sweepTimedOutTasks() {
-        int expiredJobPostingTasks = jobPostingAsyncTaskService.sweepTimedOutTasks();
-        int expiredAnalysisTasks = analysisAsyncSweepService.sweepTimedOutTasks();
+        int expiredJobPostingTasks = safeSweep("jobPosting", jobPostingAsyncTaskService::sweepTimedOutTasks);
+        int expiredAnalysisTasks = safeSweep("analysis", analysisAsyncSweepService::sweepTimedOutTasks);

         if (expiredJobPostingTasks > 0 || expiredAnalysisTasks > 0) {
             log.info(
                     "Async task sweep expired tasks. jobPostingExpired={} analysisExpired={}",
                     expiredJobPostingTasks,
                     expiredAnalysisTasks
             );
         }
     }
+
+    private int safeSweep(String name, java.util.function.IntSupplier sweep) {
+        try {
+            return sweep.getAsInt();
+        } catch (Exception e) {
+            log.error("Async task sweep failed for {}", name, e);
+            return 0;
+        }
+    }
🤖 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/global/scheduling/AsyncTaskSweepScheduler.java`
around lines 22 - 33, The sweepTimedOutTasks method in AsyncTaskSweepScheduler
runs jobPostingAsyncTaskService.sweepTimedOutTasks() and
analysisAsyncSweepService.sweepTimedOutTasks() sequentially without isolation,
so a failure in one can prevent the other from running. Update
sweepTimedOutTasks to wrap each service call independently, handling/logging
errors per call so a thrown exception from job posting does not block the
analysis sweep (and vice versa). Keep the logging around expired counts in
AsyncTaskSweepScheduler, but ensure both sweeps always get a chance to execute
each cycle.
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSweepService.java (1)

34-53: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

One failing task rolls back the entire sweep batch.

sweepTimedOutTasks() processes all timed-out tasks inside a single @Transactional method. If any single iteration throws (e.g. userService.getUser fails with USER_NOT_FOUND for a stale userId, or a DB constraint issue), the whole transaction rolls back — including the markFailed/credit-release work already done for other, unrelated tasks in that same sweep cycle. This delays recovery of all currently-stuck tasks until the next scheduled run succeeds end-to-end.

Consider processing each task in its own transaction (e.g. call a @Transactional per-task method from a non-transactional loop) and logging+skipping failures for individual tasks instead of failing the batch.

🤖 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/AnalysisAsyncSweepService.java`
around lines 34 - 53, The sweep in AnalysisAsyncSweepService.sweepTimedOutTasks
currently runs all expired-task handling inside one transaction, so a single
exception can roll back credit release and markFailed work for every task in the
batch. Refactor the loop so sweepTimedOutTasks is non-transactional and
delegates each task to a separate `@Transactional` helper (for example, a per-task
method that performs releaseCreditIfNeeded and
analysisAsyncTaskService.markFailed), and wrap each iteration with try/catch to
log the failure and continue processing the remaining AnalysisAsyncTask entries.
🤖 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/service/AnalysisAsyncSseService.java`:
- Around line 17-24: The fetch and SSE subscription in
AnalysisAsyncSseService.subscribe are not atomic, so a task can finish after
AnalysisController.streamAnalysisTask reads initialStatus but before the emitter
is registered. Move the status lookup into the subscription flow or synchronize
the read/subscribe/publish path on the same channel key so the initial snapshot
and emitter registration happen under one lock. Use the existing
AnalysisAsyncSseService.subscribe, channelKey, and sseSubscriptionRegistry
interactions as the place to enforce this.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSweepService.java`:
- Around line 34-53: The timeout sweep in
AnalysisAsyncSweepService.sweepTimedOutTasks() can race with concurrent task
completion because it scans PENDING/RUNNING tasks without any lock or version
check. Update the sweep so the mark-failed-and-refund path is atomic with
completion handling, using the AnalysisAsyncTask / analysisAsyncTaskRepository
flow to re-read or lock the task before calling releaseCreditIfNeeded() and
analysisAsyncTaskService.markFailed(). Ensure a task that has already been
completed by completeTask() cannot be refunded or overwritten as FAILED.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java`:
- Around line 52-78: The async status updates are being published too early in
AnalysisAsyncTaskService, so clients can receive a status change before the
transaction is actually committed. Update markRunning, markSuccess,
markRetryScheduled, and markFailed to defer analysisAsyncSseService.publish(...)
until after commit, using an afterCommit hook or a `@TransactionalEventListener`
with phase AFTER_COMMIT. Also apply the same pattern used by
sweepTimedOutTasks() so all SSE emissions only happen after durable state
changes.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java`:
- Around line 106-113: The credit history descriptions in AnalysisService are
still using reservation-era wording, so update the strings passed to
creditService.use in deductAnalysisCredit and creditService.refund in
refundAnalysisCredit to reflect deduct/refund semantics instead of “예약” and “예약
해제”. Keep the method names as-is and only change the transaction descriptions so
the logged history matches the current behavior.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.java`:
- Around line 42-72: Add a transactional boundary to the bridge methods so the
worker metadata update and status transition commit together. In
JobPostingWorkerBridgeService, annotate markRetry and failTask with
`@Transactional` so the calls to jobPostingAsyncTaskService.updateWorkerMetadata,
markRetryScheduled, and markFailed participate in the same transaction instead
of separate ones.

In `@src/main/java/com/jobdri/jobdri_api/global/sse/SseSubscriptionRegistry.java`:
- Around line 70-88: The exception handling in SseSubscriptionRegistry.publish()
and sendHeartbeat() is too narrow because SseEmitter.send() can also throw
IllegalStateException, which can escape and disrupt callers. Broaden the catch
in publish(), sendHeartbeat(), and the related subscribe() cleanup path to
handle any send failure, then always complete and remove the affected emitter
from the registry so stale subscriptions do not keep failing on later publishes
or heartbeats. Ensure the fix is applied in the SseSubscriptionRegistry methods
that iterate emitters and call SseEmitter.send().
- Around line 24-42: The heartbeat dispatch in SseSubscriptionRegistry is
single-threaded, so one slow SseEmitter.send() can stall heartbeats for every
other subscriber. Update SseSubscriptionRegistry so sendHeartbeatSafely (and the
channel/emitter iteration it drives) does not run all blocking writes on the
lone heartbeatExecutor thread; dispatch each channel or emitter heartbeat to a
bounded worker pool, or otherwise enforce a strict timeout around the send. Keep
the heartbeat scheduling in the constructor, but make the actual heartbeat
delivery in sendHeartbeatSafely non-blocking for unrelated subscriptions.

---

Outside diff comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java`:
- Around line 72-88: The credit reservation logic in AnalysisService.analyze()
is running inside the outer transaction, so deductAnalysisCredit() and
refundAnalysisCredit() are not committed independently before executeAnalysis()
starts. Move the reservation/refund handling into its own transactional boundary
(for example via a separate method on AnalysisService or a dedicated helper) so
the balance change commits separately, and keep analyze() focused on
validateAnalysisRequest(), prepareAnalysisExecution(), executeAnalysis(), and
finalizeAnalysis().

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java`:
- Around line 52-97: `JobPostingAsyncTaskService` is publishing SSE updates
before the enclosing transaction commits, so clients can observe uncommitted or
later-rolled-back state. Update `markRunning`, `markSuccess`,
`markRetryScheduled`, `markFailed`, and `sweepTimedOutTasks` to publish via a
post-commit hook instead of calling
`jobPostingAsyncSseService.publish(toStatusResponse(task))` directly. Add a
helper in this service (or equivalent) that registers `afterCommit()` with
`TransactionSynchronizationManager` and only publishes the
`toStatusResponse(task)` result once the transaction has successfully committed.

---

Nitpick comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSseService.java`:
- Around line 39-41: The terminal-status check in AnalysisAsyncSseService is
hardcoded with duplicated string literals, and the same logic exists in
JobPostingAsyncSseService. Replace the literal comparisons in isTerminal with
the shared domain TaskStatus enum (or a shared helper based on it) so status
checks are compile-time safe. If possible, extract the repeated
subscribe/publish/isTerminal flow into a small reusable helper used by both
services to remove the duplicated boilerplate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSweepService.java`:
- Around line 34-53: The sweep in AnalysisAsyncSweepService.sweepTimedOutTasks
currently runs all expired-task handling inside one transaction, so a single
exception can roll back credit release and markFailed work for every task in the
batch. Refactor the loop so sweepTimedOutTasks is non-transactional and
delegates each task to a separate `@Transactional` helper (for example, a per-task
method that performs releaseCreditIfNeeded and
analysisAsyncTaskService.markFailed), and wrap each iteration with try/catch to
log the failure and continue processing the remaining AnalysisAsyncTask entries.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java`:
- Around line 153-169: The credit lifecycle names in AnalysisWorkerBridgeService
still imply a reservation/hold even though the flow now performs an immediate
deduction via analysisService.deductAnalysisCredit(...). Update the helper names
and related status/marking methods to match the actual semantics (for example,
reserveCreditIfNeeded, markCreditReserved, and confirmCreditIfNeeded should
align with deduction/confirmation terminology), while keeping the existing
release/refund behavior consistent with CreditStatus and the task reference ID
handling.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerFailureRequest.java`:
- Around line 11-13: The workerId field in JobPostingWorkerFailureRequest only
has `@NotBlank`, so oversized values can slip past validation and fail later
against the JobPostingAsyncTask.workerId column length. Update the
JobPostingWorkerFailureRequest record to add `@Size`(max = 100) on workerId
alongside the existing validation annotations so the DTO matches the entity
constraint and fails fast during validation.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRetryRequest.java`:
- Around line 11-13: Add a size constraint to
JobPostingWorkerRetryRequest.workerId so it matches the 100-character limit used
by JobPostingAsyncTask and the similar JobPostingWorkerFailureRequest DTO.
Update the workerId field annotation in JobPostingWorkerRetryRequest to include
`@Size`(max = 100) alongside the existing non-blank validation, keeping validation
aligned with the entity column length.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java`:
- Around line 126-136: `JobPostingAsyncTask.markFailed` has a weaker
terminal-state check than `markRetryScheduled`, so a second failure can
overwrite an already-FAILED task. Update `markFailed(FailureReason, String,
int)` to use the same terminal guard as `isTerminal()` (or equivalent) so it
no-ops for both SUCCEEDED and FAILED states, keeping `completedAt`, `error`,
`failureReason`, and `retryCount` stable on repeat calls.

In
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java`:
- Around line 126-138: The sweep in
JobPostingAsyncTaskService.sweepTimedOutTasks() processes all PENDING/RUNNING
tasks in one transaction, which can become a long-running lock-heavy operation
as data grows. Update the repository call and loop to process tasks in bounded
batches, using a Pageable/limit or similar batch size per scheduler tick, and
keep the existing expireTimedOutTaskIfNeeded and publish(toStatusResponse(task))
flow unchanged for each processed task.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java`:
- Around line 86-95: The markConfirmationUnknown flow in
PaymentTransactionService only moves a payment to UNKNOWN and leaves no path to
recover stuck payments later. Update the payment lifecycle so UNKNOWN entries
can be reconciled automatically, ideally by adding a scheduled reconciliation
job that uses Toss inquiry by paymentKey/orderId and reuses the payment state
transitions in PaymentTransactionService. Make the fix centered around
markConfirmationUnknown and the existing payment status handling so abandoned
flows are eventually resolved without relying on the user to retry.

In
`@src/main/java/com/jobdri/jobdri_api/global/scheduling/AsyncTaskSweepScheduler.java`:
- Around line 22-33: The sweepTimedOutTasks method in AsyncTaskSweepScheduler
runs jobPostingAsyncTaskService.sweepTimedOutTasks() and
analysisAsyncSweepService.sweepTimedOutTasks() sequentially without isolation,
so a failure in one can prevent the other from running. Update
sweepTimedOutTasks to wrap each service call independently, handling/logging
errors per call so a thrown exception from job posting does not block the
analysis sweep (and vice versa). Keep the logging around expired counts in
AsyncTaskSweepScheduler, but ensure both sweeps always get a chance to execute
each cycle.

In
`@src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java`:
- Around line 138-149: The new isTerminal() guard in markRetryScheduled is not
covered by tests; add a test in JobPostingAsyncTaskServiceTest that calls
markRetryScheduled on an already terminal JobPostingAsyncTask (SUCCEEDED or
FAILED) and verifies it remains unchanged, similar to
AnalysisWorkerBridgeServiceTest.terminalTaskDoesNotReopen. Use the existing
markRetryScheduled and isTerminal behavior to keep the assertion focused on the
no-op path.
- Around line 118-136: The max-retry failure test in
JobPostingAsyncTaskServiceTest only checks the entity state transition; update
it to also verify that JobPostingAsyncSseService.publish(...) is invoked when
markRetryScheduled(...) auto-transitions a task to FAILED. Use the existing task
setup and add a verification for the publish call on the max-retry exhaustion
path so the test covers the new SSE behavior introduced by
JobPostingAsyncTaskService.markRetryScheduled.

In
`@src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java`:
- Around line 186-221: Add a test to cover the UNKNOWN retry path when the
confirm request uses a different paymentKey than the stored payment. Extend
PaymentServiceTest around confirmAllowsRetryWhenTossConfirmTimesOut() to first
force a timeout, then call paymentService.confirm with a mismatched
PaymentConfirmRequest and assert GeneralException with
GeneralErrorCode.PAYMENT_ALREADY_PROCESSED. Use the existing
paymentService.confirm, TossPaymentConfirmResponse, and
paymentRepository.findByOrderId setup to exercise the startConfirmation branch
for UNKNOWN payments.
🪄 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: f62b624e-3010-4ee1-b143-dc56f4aff7ef

📥 Commits

Reviewing files that changed from the base of the PR and between 945905f and 0faa17a.

📒 Files selected for processing (36)
  • src/main/java/com/jobdri/jobdri_api/JobdriApiApplication.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAdminDebugService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSseService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncSweepService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisExecutionPayload.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisImprovementRules.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisQueueProperties.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingWorkerInternalController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerFailureRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRetryRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingAsyncTaskRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncSseService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/PaymentStatus.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java
  • src/main/java/com/jobdri/jobdri_api/global/scheduling/AsyncTaskSweepScheduler.java
  • src/main/java/com/jobdri/jobdri_api/global/sse/SseSubscriptionRegistry.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java

async SSE 발행을 트랜잭션 커밋 이후로 지연해 클라이언트가 미커밋 상태를 관측하지 않도록 수정하고, SSE 구독/하트비트 예외 처리와 채널별 동기화를 보강했습니다. 또한 analysis sweep를 per-task 트랜잭션으로 분리하고 scheduler 예외 격리를 추가했으며, 채용공고 worker retry/failed 트랜잭션 일관성 및 DTO 검증을 강화해 비동기 상태 복구 경로의 안정성을 높였습니다.:wq
analysis async stuck-task sweep에 per-task 트랜잭션을 적용하는 과정에서 발생한 순환 의존성을 제거했습니다. self-injection 대신 TransactionTemplate 기반으로 각 task를 개별 트랜잭션에서 처리하도록 변경해 스프링 컨텍스트 초기화 실패와 테스트 전반의 연쇄 실패를 해소했습니다.
@shinae1023 shinae1023 merged commit 8cc51f0 into dev Jul 7, 2026
2 of 3 checks passed
shinae1023 added a commit that referenced this pull request Jul 7, 2026
[Feat] SSE 기반 알림 및 읽음 처리 기능 추가 (#120)
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.

1 participant