Skip to content

[Fix] 모의지원 홈 응답에 분석 taskId 추가 - #233

Merged
whc9999 merged 3 commits into
devfrom
fix/mock-apply-home-task-id
Jul 30, 2026
Merged

[Fix] 모의지원 홈 응답에 분석 taskId 추가#233
whc9999 merged 3 commits into
devfrom
fix/mock-apply-home-task-id

Conversation

@whc9999

@whc9999 whc9999 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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

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

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

  • MockApplyHomeItemResponse에 nullable taskId 필드 추가
  • 진행 중인 분석 작업을 mockApplyId 기준으로 매핑해 taskId 응답
  • PENDING/RUNNING 분석 작업의 taskId 응답 테스트 보강

📸 작업 화면 스크린샷

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

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

🚨 관련 이슈 번호 [ ]

Summary by CodeRabbit

  • 새 기능

    • PortOne 결제 제공자 연동을 지원합니다.
    • 결제 준비, 완료 검증, 결제 상태 조회 및 웹훅 처리를 추가했습니다.
    • 결제 상태와 외부 거래 정보가 결제 내역에 반영됩니다.
    • 결제 완료 및 웹훅 중복 처리 시 크레딧이 중복 지급되지 않습니다.
    • 진행 중인 분석 항목에 작업 식별자가 표시됩니다.
  • 보안

    • PortOne 웹훅 서명과 요청 유효성을 검증합니다.

whc9999 added 2 commits July 30, 2026 23:34
- 기존 토스페이 직접 연동을 유지하면서 결제 provider 분기 추가
- PORTONE 결제 준비 응답에 Store ID, Channel Key, redirect URL 반환
- 포트원 결제 완료 검증 API 및 웹훅 API 추가
- 포트원 결제 단건 조회 결과 기반으로 금액, 통화, 상태 검증
- Payment 외부 결제 공통 필드 및 schema.sql 컬럼 추가
- 비관적 락과 완료 상태 선확인으로 중복 크레딧 지급 방지
- 포트원 환경변수 및 테스트 케이스 추가
- MockApplyHomeItemResponse에 nullable taskId 필드 추가
- 진행 중인 분석 작업을 mockApplyId 기준으로 매핑해 taskId 응답
- PENDING/RUNNING 분석 작업의 taskId 응답 테스트 보강
@whc9999 whc9999 self-assigned this Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 37 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5d44176-00ef-49c8-975f-599204b643a0

📥 Commits

Reviewing files that changed from the base of the PR and between b87a90a and f7f86d0.

📒 Files selected for processing (11)
  • src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/security/PortOneWebhookRateLimitFilter.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/domain/payment/service/PortOneClient.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneWebhookVerifier.java
  • src/main/java/com/jobdri/jobdri_api/global/config/SecurityConfig.java
  • src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClientTest.java
  • src/test/resources/application-test.yaml
📝 Walkthrough

Walkthrough

PortOne 결제 제공자, 결제 준비·승인·웹훅 처리와 외부 상태 저장이 추가되었습니다. 결제 설정과 스키마가 확장되었으며, 분석 홈 응답에는 활성 작업의 taskId가 포함됩니다.

Changes

PortOne 결제 연동

Layer / File(s) Summary
결제 계약과 상태 모델
src/main/java/com/jobdri/jobdri_api/domain/payment/dto/..., src/main/java/com/jobdri/jobdri_api/domain/payment/entity/..., src/main/resources/schema.sql
PortOne DTO와 제공자·상태 enum이 추가되고, Payment 및 결제 응답에 외부 식별자·상태·제공자 정보가 반영됩니다. 결제 테이블 컬럼과 외부 결제 ID 유니크 인덱스도 추가됩니다.
PortOne API와 웹훅 검증
src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClient.java, src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneWebhookVerifier.java, src/main/java/com/jobdri/jobdri_api/global/config/SecurityConfig.java, .env*, src/main/resources/application-*.yaml
PortOne API 조회, 설정 검증, 타임아웃·예외 매핑, 웹훅 HMAC-SHA256 서명 검증 및 웹훅 경로 공개 접근 설정이 추가됩니다.
결제 준비·승인·웹훅 흐름
src/main/java/com/jobdri/jobdri_api/domain/payment/controller/PaymentController.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
제공자별 결제 준비 분기와 PortOne 완료·웹훅 엔드포인트가 추가되고, 결제 응답 검증 후 상태 전이와 크레딧 처리가 수행됩니다.
결제 흐름 검증
src/test/java/com/jobdri/jobdri_api/domain/payment/service/*, src/test/resources/application-test.yaml
PortOne 준비, 승인 상태·금액·소유권 검증, 동시 처리 멱등성, 웹훅 서명 및 재수신 동작을 테스트합니다.

분석 작업 식별자

Layer / File(s) Summary
홈 응답 taskId 매핑
src/main/java/com/jobdri/jobdri_api/domain/mockapply/..., src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java
활성 분석 작업을 mockApplyIdtaskId로 매핑하고, 진행 중·대기·완료 항목의 taskId 반환값을 검증합니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PaymentController
  participant PaymentService
  participant PortOneClient
  participant PaymentTransactionService
  Client->>PaymentController: PortOne 결제 완료 요청
  PaymentController->>PaymentService: completePortOne()
  PaymentService->>PortOneClient: getPayment(paymentId)
  PortOneClient-->>PaymentService: PortOne 결제 응답
  PaymentService->>PaymentTransactionService: applyPortOnePayment()
  PaymentTransactionService-->>PaymentService: 결제 확인 결과
  PaymentService-->>PaymentController: API 응답
  PortOneClient-->>PaymentService: 웹훅 결제 조회
  PaymentService->>PaymentTransactionService: applyPortOneWebhookPayment()
Loading

Possibly related PRs

Suggested labels: 🐛 fix, ✨ feat

🚥 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 제목이 모의지원 홈 응답에 분석 taskId를 추가하는 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 요구된 템플릿 섹션이 대부분 채워져 있고, 변경 이유와 작업 내용도 구체적으로 작성되었습니다.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mock-apply-home-task-id

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: 17

🤖 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/mockapply/service/MockApplyService.java`:
- Around line 325-343: Update findActiveAnalysisTaskIds so duplicate active
tasks for the same mockApplyId deterministically retain the newest task by
createdAt descending, rather than relying on repository result order; apply this
ordering before the existing toMap merge or enforce an equivalent repository/DB
constraint guaranteeing one active task per mockApplyId.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java`:
- Around line 132-156: PaymentService.preparePortOne currently creates and
attaches the PortOne payment in separate transactions, leaving unrecoverable
PENDING records if attachment fails. Add a dedicated PaymentTransactionService
method such as createPortOnePendingPayment that creates the payment and sets its
PortOne payment ID within one transaction, then replace the separate
createPendingPayment and attachPortOnePayment calls in preparePortOne with that
method while preserving the existing orderId and response behavior.
- Around line 234-259: Update handlePortOneWebhook’s GeneralException handling
to treat PAYMENT_AMOUNT_MISMATCH as an ignored permanent failure alongside the
existing non-retryable codes, returning successfully after logging it. Preserve
propagation for transient failures such as SERVICE_UNAVAILABLE and
EXTERNAL_SERVICE_TIMEOUT so PortOne retries remain effective only when recovery
may be possible.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java`:
- Around line 32-55: Payment 생성과 PortOne 연결이 별도 트랜잭션으로 실행되지 않도록
PaymentTransactionService의 createPendingPayment 및 attachPortOnePayment 흐름을 하나의
원자적 createPendingPortOnePayment 메서드로 통합하고, 생성·attach를 동일 트랜잭션에서 처리하세요.
PaymentService.preparePortOne은 기존 두 단계 호출 대신 이 통합 메서드를 사용하도록 변경해
provider=PORTONE인 미완성 결제 row가 커밋되지 않게 하세요.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClient.java`:
- Around line 90-102: Update the getPayment retry flow to retry idempotent GET
requests for PortOne 5xx responses and timeout ResourceAccessException failures,
using 1–2 attempts with short exponential backoff. Define an explicit retry
limit and total timeout budget, then preserve the existing GeneralException
mappings after retries are exhausted and avoid retrying other client errors.
- Around line 50-59: Update PortOneClient.init() to replace
SimpleClientHttpRequestFactory with a pooling-capable Apache HttpClient5
connection manager or JdkClientHttpRequestFactory, matching the existing payment
client approach. Configure the 5-second connect and 10-second read/response
timeouts on the selected factory or HTTP client, then pass it to
restClientBuilder without changing the base URL.
- Around line 83-89: Update PortOneClient’s payment retrieval method to detect a
null result from retrieve().body(PortOnePaymentResponse.class) and convert it
into the service’s established domain exception before returning. Ensure
PaymentTransactionService.validatePortOnePayment never receives a null response
and preserves the existing domain-error flow.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneWebhookVerifier.java`:
- Around line 97-101: Update secretBytes() to use the whsec_ prefix as the
encoding discriminator: strip the prefix and Base64-decode only when it is
present; otherwise return webhookSecret UTF-8 bytes directly. Remove the
fallback that Base64-decodes unprefixed plaintext secrets.

In `@src/main/java/com/jobdri/jobdri_api/global/config/SecurityConfig.java`:
- Line 103: Review the public `/api/payments/portone/webhook` endpoint around
`SecurityConfig` and add an appropriate defense-in-depth control, such as rate
limiting or trusted PortOne source-IP validation, in addition to the existing
`portOneWebhookVerifier.verify(rawBody, headers)` HMAC check. Preserve
`permitAll` and ensure the control runs before webhook state changes.

In `@src/main/resources/application-prod.yaml`:
- Line 191: Align the production PortOne redirect URL with the Toss redirect
domain and avoid conflicting hardcoded defaults: update
src/main/resources/application-prod.yaml at line 191 to use the established
production domain or an empty ${PORTONE_REDIRECT_URL:} default, and update
.env.production.example at line 61 to match the Toss redirect domain used on
lines 53-54.

In `@src/main/resources/schema.sql`:
- Around line 157-160: Update the idx_payments_external_payment_id_unique
creation to use concurrent index creation, matching the requested
deployment-safe pattern and preserving the existing partial unique constraint.
Ensure the migration or initialization path executes this statement outside an
encompassing transaction, as required for concurrent index creation.

In
`@src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java`:
- Around line 347-349: Update MockApplyServiceTest around the analysis task
fixtures and assertions to cover incomplete applications with no task and with
FAILED or CANCELLED tasks. Assert each such case returns
analysisInProgress=false and taskId=null, while preserving the existing
PENDING/RUNNING and COMPLETED coverage.

In
`@src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java`:
- Around line 777-809: PaymentServiceTest의 handlePortOneWebhook 예외 분기와 재시도 동작에
대한 검증이 부족합니다. 존재하지 않는 paymentId는 예외 없이 무시되는지, 금액 불일치는 웹훅 재시도를 유도하도록 전파되는지,
portOneClient.getPayment가 EXTERNAL_SERVICE_TIMEOUT을 던질 때 해당 예외가 전파되는지 테스트를
추가하세요. 기존 portOneWebhook 호출 방식과 예외 코드 검증 패턴을 재사용해 각 경로의 응답 및 후속 상태를 확인하세요.
- Around line 756-767: Update the concurrent task setup around runConcurrently
in PaymentServiceTest so each task receives an explicit role or index and
branches deterministically between portOneWebhook and completePortOne. Remove
the Thread.currentThread().getName().endsWith("1") dependency, while preserving
the requirement that one task invokes each operation concurrently.
- Around line 880-900: Update signPortOneWebhook and its test fixture secret to
use the same whsec_ plus base64-encoded representation consumed by
PortOneWebhookVerifier, or reuse the configured payment.portone.webhook-secret
value through signedWebhookHeaders. Ensure the signing helper decodes the
configured secret before creating the HMAC key so the test exercises the
verifier’s actual parsing path.

In
`@src/test/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClientTest.java`:
- Around line 37-62: Extend PortOneClientTest beyond init() to cover getPayment
error mapping and boundary cases: verify 5xx and other RestClientException
failures map to SERVICE_UNAVAILABLE, timeouts map to EXTERNAL_SERVICE_TIMEOUT,
blank paymentId is validated, and an empty response body is handled correctly.
Use MockRestServiceServer or an injected stub request factory to simulate the
relevant HTTP responses and timeout, while preserving the existing
initialization tests.

In `@src/test/resources/application-test.yaml`:
- Around line 95-102: Update the test profile’s portone.api-base-url
configuration to a local dummy address instead of https://api.portone.io, while
leaving the remaining test fixture values unchanged.
🪄 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: 07d5a9fb-b5b5-4cd4-937d-93fc491b3bba

📥 Commits

Reviewing files that changed from the base of the PR and between 80e7bb5 and b87a90a.

📒 Files selected for processing (29)
  • .env.example
  • .env.production.example
  • src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/controller/PaymentController.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOneAmount.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOnePaymentResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOnePrepareData.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOneWebhookPayload.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/PaymentPrepareRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/PortOnePaymentCompleteRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentOrderStatusResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentPrepareResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/PaymentProviderType.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/PortOnePaymentStatus.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/domain/payment/service/PortOneClient.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneWebhookVerifier.java
  • src/main/java/com/jobdri/jobdri_api/global/config/SecurityConfig.java
  • src/main/resources/application-analysis-eval.yaml
  • src/main/resources/application-dev.yaml
  • src/main/resources/application-prod.yaml
  • src/main/resources/schema.sql
  • src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClientTest.java
  • src/test/resources/application-test.yaml

Comment on lines +325 to +343
private Map<Long, String> findActiveAnalysisTaskIds(Long userId, List<MockApply> mockApplies) {
List<Long> mockApplyIds = mockApplies.stream()
.filter(mockApply -> mockApply.getStatus() != MockApplyStatus.COMPLETED)
.map(MockApply::getId)
.toList();
if (mockApplyIds.isEmpty()) {
return Set.of();
return Map.of();
}
return analysisAsyncTaskRepository.findByUserIdAndMockApplyIdInAndStatusIn(
userId,
mockApplyIds,
List.of(TaskStatus.PENDING, TaskStatus.RUNNING)
)
.stream()
.map(task -> task.getMockApplyId())
.collect(Collectors.toSet());
.collect(Collectors.toMap(
task -> task.getMockApplyId(),
task -> task.getTaskId(),
(first, ignored) -> first
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -u
rg -n -C 4 \
  'interface AnalysisAsyncTaskRepository|findByUserIdAndMockApplyIdInAndStatusIn|class AnalysisAsyncTask|`@UniqueConstraint`|CREATE TABLE.*analysis' \
  src/main/java src/main/resources || true

Repository: JobDri-Developer/BackEnd

Length of output: 13888


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '\n== MockApplyService snippet ==\n'
sed -n '300,360p' src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java

printf '\n== AnalysisAsyncTask entity ==\n'
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.java

printf '\n== AnalysisAsyncTaskRepository ==\n'
sed -n '1,200p' src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java

printf '\n== Search for status transitions / creation paths ==\n'
rg -n -C 3 'PENDING|RUNNING|COMPLETED|create.*AnalysisAsyncTask|AnalysisAsyncTask\.builder|save\(.*AnalysisAsyncTask|update.*TaskStatus|setStatus\(' src/main/java/com/jobdri/jobdri_api/domain/analysis src/main/java/com/jobdri/jobdri_api/domain/mockapply || true

Repository: JobDri-Developer/BackEnd

Length of output: 32754


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '\n== AnalysisAsyncTaskService outline ==\n'
ast-grep outline src/main/java/com/jobdri/jobdri_api/domain/analysis/service/async/AnalysisAsyncTaskService.java --view expanded || true

printf '\n== MockApplyService outline ==\n'
ast-grep outline src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java --view expanded || true

printf '\n== Creation / status update search ==\n'
rg -n -C 4 'new AnalysisAsyncTask|AnalysisAsyncTask\.builder|taskStatus|TaskStatus\.PENDING|TaskStatus\.RUNNING|TaskStatus\.COMPLETED|save\(.*analysis_async|save\(analysisAsyncTask|update.*AnalysisAsyncTask|setTaskId|setStatus\(' src/main/java/com/jobdri/jobdri_api/domain/analysis src/main/java/com/jobdri/jobdri_api/domain/mockapply || true

Repository: JobDri-Developer/BackEnd

Length of output: 15022


활성 task는 최신순으로 고르세요.
findByUserIdAndMockApplyIdInAndStatusIn(...) 결과는 정렬이 없어서 toMap(..., (first, ignored) -> first)가 조회 순서에 의존합니다. 같은 mockApplyIdPENDING/RUNNING task가 여러 개면 오래된 taskId가 남을 수 있으니, createdAt desc 기준으로 최신 1건만 선택하거나 DB 제약으로 활성 task를 1개로 제한하세요.

🤖 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/mockapply/service/MockApplyService.java`
around lines 325 - 343, Update findActiveAnalysisTaskIds so duplicate active
tasks for the same mockApplyId deterministically retain the newest task by
createdAt descending, rather than relying on repository result order; apply this
ordering before the existing toMap merge or enforce an equivalent repository/DB
constraint guaranteeing one active task per mockApplyId.

Comment on lines +32 to +55
return createPendingPayment(userId, plan, orderId, PaymentProviderType.TOSS_PAY_DIRECT);
}

@Transactional
public Payment createPendingPayment(Long userId, CreditPlan plan, String orderId, PaymentProviderType provider) {
User user = userService.getUser(userId);
return paymentRepository.save(Payment.createPending(
user,
"JobDri 크레딧 " + plan.getName(),
orderId,
plan.getCode(),
plan.getCreditAmount(),
plan.getPrice()
plan.getPrice(),
provider
));
}

@Transactional
public Payment attachPortOnePayment(Long userId, String orderId) {
Payment payment = getOwnedPaymentForUpdate(userId, orderId);
payment.attachPortOnePayment(orderId);
return payment;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

결제 생성과 PortOne attach가 트랜잭션 2개로 분리되어 있어 부분 실패 시 orphan row 발생 가능

createPendingPayment(..., PORTONE)attachPortOnePayment(userId, orderId)가 각각 독립된 @Transactional로 실행되고, 두 호출은 PaymentService.preparePortOne에서 순차 호출됩니다(컨텍스트 스니펫 확인). 첫 트랜잭션 커밋 후 두 번째 트랜잭션 실행 전에 재시작/장애가 발생하면 provider=PORTONE이지만 externalPaymentId=null인 결제 row가 남습니다.

이 상태에서 실제 PortOne 웹훅/완료 요청이 들어와도 validatePortOnePaymentexternalPaymentId null 체크에 걸려 PAYMENT_CONFIRM_FAILED가 발생하고, handlePortOneWebhook은 이 에러코드를 조용히 무시하도록 되어 있어 자동 복구 경로가 없습니다. 두 단계를 하나의 트랜잭션으로 합치면 이 리스크를 제거할 수 있습니다.

As per path instructions, "비동기 처리 안정성... 부분 실패 복구 가능성"을 우선순위 높게 검토해야 합니다.

♻️ 원자적 생성 메서드로 통합하는 제안
     `@Transactional`
     public Payment createPendingPayment(Long userId, CreditPlan plan, String orderId, PaymentProviderType provider) {
         User user = userService.getUser(userId);
         return paymentRepository.save(Payment.createPending(
                 user,
                 "JobDri 크레딧 " + plan.getName(),
                 orderId,
                 plan.getCode(),
                 plan.getCreditAmount(),
                 plan.getPrice(),
                 provider
         ));
     }
 
+    `@Transactional`
+    public Payment createPendingPortOnePayment(Long userId, CreditPlan plan, String orderId) {
+        User user = userService.getUser(userId);
+        Payment payment = Payment.createPending(
+                user,
+                "JobDri 크레딧 " + plan.getName(),
+                orderId,
+                plan.getCode(),
+                plan.getCreditAmount(),
+                plan.getPrice(),
+                PaymentProviderType.PORTONE
+        );
+        payment.attachPortOnePayment(orderId);
+        return paymentRepository.save(payment);
+    }
+
     `@Transactional`
     public Payment attachPortOnePayment(Long userId, String orderId) {
         Payment payment = getOwnedPaymentForUpdate(userId, orderId);
         payment.attachPortOnePayment(orderId);
         return payment;
     }

PaymentService.preparePortOne(현재 리뷰 범위 밖)에서도 두 단계 호출 대신 createPendingPortOnePayment 단일 호출로 교체가 필요합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return createPendingPayment(userId, plan, orderId, PaymentProviderType.TOSS_PAY_DIRECT);
}
@Transactional
public Payment createPendingPayment(Long userId, CreditPlan plan, String orderId, PaymentProviderType provider) {
User user = userService.getUser(userId);
return paymentRepository.save(Payment.createPending(
user,
"JobDri 크레딧 " + plan.getName(),
orderId,
plan.getCode(),
plan.getCreditAmount(),
plan.getPrice()
plan.getPrice(),
provider
));
}
@Transactional
public Payment attachPortOnePayment(Long userId, String orderId) {
Payment payment = getOwnedPaymentForUpdate(userId, orderId);
payment.attachPortOnePayment(orderId);
return payment;
}
return createPendingPayment(userId, plan, orderId, PaymentProviderType.TOSS_PAY_DIRECT);
}
`@Transactional`
public Payment createPendingPayment(Long userId, CreditPlan plan, String orderId, PaymentProviderType provider) {
User user = userService.getUser(userId);
return paymentRepository.save(Payment.createPending(
user,
"JobDri 크레딧 " + plan.getName(),
orderId,
plan.getCode(),
plan.getCreditAmount(),
plan.getPrice(),
provider
));
}
`@Transactional`
public Payment createPendingPortOnePayment(Long userId, CreditPlan plan, String orderId) {
User user = userService.getUser(userId);
Payment payment = Payment.createPending(
user,
"JobDri 크레딧 " + plan.getName(),
orderId,
plan.getCode(),
plan.getCreditAmount(),
plan.getPrice(),
PaymentProviderType.PORTONE
);
payment.attachPortOnePayment(orderId);
return paymentRepository.save(payment);
}
`@Transactional`
public Payment attachPortOnePayment(Long userId, String orderId) {
Payment payment = getOwnedPaymentForUpdate(userId, orderId);
payment.attachPortOnePayment(orderId);
return payment;
}
🤖 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 32 - 55, Payment 생성과 PortOne 연결이 별도 트랜잭션으로 실행되지 않도록
PaymentTransactionService의 createPendingPayment 및 attachPortOnePayment 흐름을 하나의
원자적 createPendingPortOnePayment 메서드로 통합하고, 생성·attach를 동일 트랜잭션에서 처리하세요.
PaymentService.preparePortOne은 기존 두 단계 호출 대신 이 통합 메서드를 사용하도록 변경해
provider=PORTONE인 미완성 결제 row가 커밋되지 않게 하세요.

Source: Path instructions

Comment thread src/test/resources/application-test.yaml
- 포트원 결제 준비 시 Payment 생성과 externalPaymentId 연결을 단일 트랜잭션으로 통합
- 포트원 결제 조회에 5xx/timeout 재시도와 null 응답 검증 추가
- 포트원 웹훅 secret 파싱을 whsec_ prefix 기준으로 정리
- 포트원 웹훅 금액 불일치 등 영구 실패는 로그 후 재시도 없이 처리
- 포트원 웹훅 엔드포인트에 인메모리 rate limit 방어 추가
- 홈 화면 분석 진행 작업 선택 시 최신 active task를 결정적으로 선택
- 결제/홈 화면 관련 회귀 테스트 보강
@whc9999
whc9999 merged commit 56aa660 into dev Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant