[Fix] 모의지원 홈 응답에 분석 taskId 추가 - #233
Conversation
- 기존 토스페이 직접 연동을 유지하면서 결제 provider 분기 추가 - PORTONE 결제 준비 응답에 Store ID, Channel Key, redirect URL 반환 - 포트원 결제 완료 검증 API 및 웹훅 API 추가 - 포트원 결제 단건 조회 결과 기반으로 금액, 통화, 상태 검증 - Payment 외부 결제 공통 필드 및 schema.sql 컬럼 추가 - 비관적 락과 완료 상태 선확인으로 중복 크레딧 지급 방지 - 포트원 환경변수 및 테스트 케이스 추가
- MockApplyHomeItemResponse에 nullable taskId 필드 추가 - 진행 중인 분석 작업을 mockApplyId 기준으로 매핑해 taskId 응답 - PENDING/RUNNING 분석 작업의 taskId 응답 테스트 보강
|
Warning Review limit reached
Next review available in: 37 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 (11)
📝 WalkthroughWalkthroughPortOne 결제 제공자, 결제 준비·승인·웹훅 처리와 외부 상태 저장이 추가되었습니다. 결제 설정과 스키마가 확장되었으며, 분석 홈 응답에는 활성 작업의 ChangesPortOne 결제 연동
분석 작업 식별자
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()
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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: 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
📒 Files selected for processing (29)
.env.example.env.production.examplesrc/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/controller/PaymentController.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOneAmount.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOnePaymentResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOnePrepareData.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/portone/PortOneWebhookPayload.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/PaymentPrepareRequest.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/PortOnePaymentCompleteRequest.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentOrderStatusResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/dto/response/PaymentPrepareResponse.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/entity/PaymentProviderType.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/entity/PortOnePaymentStatus.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClient.javasrc/main/java/com/jobdri/jobdri_api/domain/payment/service/PortOneWebhookVerifier.javasrc/main/java/com/jobdri/jobdri_api/global/config/SecurityConfig.javasrc/main/resources/application-analysis-eval.yamlsrc/main/resources/application-dev.yamlsrc/main/resources/application-prod.yamlsrc/main/resources/schema.sqlsrc/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.javasrc/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.javasrc/test/java/com/jobdri/jobdri_api/domain/payment/service/PortOneClientTest.javasrc/test/resources/application-test.yaml
| 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 | ||
| )); |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: JobDri-Developer/BackEnd
Length of output: 15022
활성 task는 최신순으로 고르세요.
findByUserIdAndMockApplyIdInAndStatusIn(...) 결과는 정렬이 없어서 toMap(..., (first, ignored) -> first)가 조회 순서에 의존합니다. 같은 mockApplyId에 PENDING/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.
| 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; | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ 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 웹훅/완료 요청이 들어와도 validatePortOnePayment의 externalPaymentId 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.
| 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
- 포트원 결제 준비 시 Payment 생성과 externalPaymentId 연결을 단일 트랜잭션으로 통합 - 포트원 결제 조회에 5xx/timeout 재시도와 null 응답 검증 추가 - 포트원 웹훅 secret 파싱을 whsec_ prefix 기준으로 정리 - 포트원 웹훅 금액 불일치 등 영구 실패는 로그 후 재시도 없이 처리 - 포트원 웹훅 엔드포인트에 인메모리 rate limit 방어 추가 - 홈 화면 분석 진행 작업 선택 시 최신 active task를 결정적으로 선택 - 결제/홈 화면 관련 회귀 테스트 보강
✨ 어떤 이유로 PR를 하셨나요?
📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요
📸 작업 화면 스크린샷
🚨 관련 이슈 번호 [ ]
Summary by CodeRabbit
새 기능
보안