Feat: credit fix [JDDEV-73] - #109
Conversation
📝 WalkthroughWalkthroughThe credit purchase flow now uses a confirmation modal in ChangesCredit purchase flow
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Customer
participant CreditCard
participant PurchaseAPI
participant TossPayments
participant CreditContent
Customer->>CreditCard: Select credit plan
CreditCard->>PurchaseAPI: preparePurchase(planCode)
PurchaseAPI-->>CreditCard: Payment data
CreditCard->>TossPayments: requestPayment()
TossPayments-->>CreditContent: paymentKey, orderId, amount
CreditContent->>PurchaseAPI: confirmPurchase()
PurchaseAPI-->>CreditContent: Confirmation result
CreditContent-->>Customer: Alert result and reload page
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 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: 2
🧹 Nitpick comments (1)
jobdri/src/components/common/modal/ModalOverlay.tsx (1)
10-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider also closing on Escape.
Click-outside-to-close was added here; consider pairing it with an Escape-key handler for consistency and keyboard accessibility, since this overlay now gates a payment confirmation flow.
♻️ Proposed addition
export function ModalOverlay({ children, onClose }: ModalOverlayProps) { useEffect(() => { document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = "unset"; }; }, []); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose?.(); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + 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 `@jobdri/src/components/common/modal/ModalOverlay.tsx` around lines 10 - 28, Add Escape-key handling to ModalOverlay alongside the existing click-outside onClose behavior. Register a keydown listener in the useEffect that invokes onClose when event.key is Escape, and remove the listener during cleanup while preserving the body overflow restoration.
🤖 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 `@jobdri/src/app/credit/page.tsx`:
- Around line 40-63: Update the confirmation flow in the useEffect around
processConfirm so window.history.replaceState removes the payment query
parameters before calling confirmPurchase. Keep the captured paymentKey,
orderId, and amount available for the in-flight request, while ensuring
refreshes or effect re-runs no longer see the original parameters and trigger
duplicate confirmations.
In `@jobdri/src/components/common/cards/CreditCard.tsx`:
- Around line 55-90: Update handleConfirmPurchase to return immediately when
isLoading is already true, before setting loading state or calling
preparePurchase, preventing concurrent purchase requests from rapid repeated
invocation. Preserve the existing purchase flow and cleanup behavior for the
first invocation.
---
Nitpick comments:
In `@jobdri/src/components/common/modal/ModalOverlay.tsx`:
- Around line 10-28: Add Escape-key handling to ModalOverlay alongside the
existing click-outside onClose behavior. Register a keydown listener in the
useEffect that invokes onClose when event.key is Escape, and remove the listener
during cleanup while preserving the body overflow restoration.
🪄 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: b01c64bd-75cc-48f3-b767-cec2138a1597
📒 Files selected for processing (8)
jobdri/src/app/credit/page.tsxjobdri/src/components/common/cards/CreditCard.tsxjobdri/src/components/common/credit/CreditTable.tsxjobdri/src/components/common/lnb/Lnb.tsxjobdri/src/components/common/modal/ModalCard.tsxjobdri/src/components/common/modal/ModalOverlay.tsxjobdri/src/components/common/modal/ModalPurchase.tsxjobdri/src/components/common/modal/index.ts
💤 Files with no reviewable changes (2)
- jobdri/src/components/common/modal/ModalPurchase.tsx
- jobdri/src/components/common/modal/index.ts
| if (paymentKey && orderId && amount) { | ||
| const processConfirm = async () => { | ||
| setIsConfirming(true); // 화면 잠금 (중복 요청 방지) | ||
| setIsConfirming(true); // 화면 잠금 | ||
|
|
||
| try { | ||
| await confirmPurchase(paymentKey, orderId, Number(amount)); | ||
|
|
||
| alert("크레딧 충전이 완료되었습니다!"); | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| setIsConfirming(false); | ||
| setTimeout(() => { | ||
| alert("크레딧 충전이 완료되었습니다!"); | ||
| window.location.reload(); | ||
| }, 100); | ||
| } catch (error) { | ||
| console.error("결제 승인 실패:", error); | ||
| alert("결제 승인에 실패했습니다. 다시 시도해 주세요."); | ||
| } finally { | ||
| setIsConfirming(false); | ||
| // 새로고침 시 중복 호출 방지 | ||
| router.replace("/credit"); | ||
| alert("결제 승인에 실패했습니다. 다시 시도해 주세요."); | ||
| // 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지 | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| } | ||
| }; | ||
|
|
||
| processConfirm(); | ||
| } | ||
| }, [searchParams, router]); | ||
| }, [searchParams]); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Query params are cleared after confirmation, not before — risks duplicate confirmPurchase calls.
The stated design ("clears them before confirmation") isn't what the code does: confirmPurchase runs first, and window.history.replaceState only clears paymentKey/orderId/amount afterward. If the user refreshes the page while the confirm request is still in flight (or the effect re-fires for any reason), the same query params are still present and processConfirm will run again with the same paymentKey, sending a duplicate confirmation request for the same payment.
🐛 Proposed fix
-import { useEffect, useState, Suspense } from "react";
+import { useEffect, useRef, useState, Suspense } from "react";
@@
function CreditContent() {
const [plans, setPlans] = useState<CreditPlan[]>([]);
const [isConfirming, setIsConfirming] = useState(false); // 승인 중 로딩 상태 추가
const searchParams = useSearchParams();
const router = useRouter(); // 라우터 훅 추가
+ const hasProcessedPaymentRef = useRef(false);
@@
if (paymentKey && orderId && amount) {
+ if (paymentKey && orderId && amount && !hasProcessedPaymentRef.current) {
+ hasProcessedPaymentRef.current = true;
const processConfirm = async () => {
setIsConfirming(true); // 화면 잠금
+ // Clear the query params immediately so a refresh/remount can't replay this confirmation.
+ window.history.replaceState(null, "", window.location.pathname);
try {
await confirmPurchase(paymentKey, orderId, Number(amount));
- window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
setTimeout(() => {
alert("크레딧 충전이 완료되었습니다!");
window.location.reload();
}, 100);
} catch (error) {
console.error("결제 승인 실패:", error);
setIsConfirming(false);
alert("결제 승인에 실패했습니다. 다시 시도해 주세요.");
- // 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지
- window.history.replaceState(null, "", window.location.pathname);
}
};
processConfirm();
}🤖 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 `@jobdri/src/app/credit/page.tsx` around lines 40 - 63, Update the confirmation
flow in the useEffect around processConfirm so window.history.replaceState
removes the payment query parameters before calling confirmPurchase. Keep the
captured paymentKey, orderId, and amount available for the in-flight request,
while ensuring refreshes or effect re-runs no longer see the original parameters
and trigger duplicate confirmations.
| const handleConfirmPurchase = async () => { | ||
| setIsLoading(true); | ||
| try { | ||
| const { clientKey, orderId, orderName, amount, customerEmail } = | ||
| await preparePurchase(planCode); | ||
|
|
||
| const tossPayments = await loadTossPayments(clientKey); | ||
| const payment = tossPayments.payment({ customerKey: ANONYMOUS }); | ||
|
|
||
| await payment.requestPayment({ | ||
| method: "CARD", | ||
| amount: { currency: "KRW", value: amount }, | ||
| orderId, | ||
| orderName, | ||
| customerEmail, | ||
| successUrl: `${window.location.origin}/credit`, | ||
| failUrl: `${window.location.origin}/credit/fail`, | ||
| card: { | ||
| easyPay: "TOSSPAY", | ||
| flowMode: "DIRECT", | ||
| }, | ||
| }); | ||
| } catch (error: unknown) { | ||
| const tossError = error as { code?: string; message?: string }; | ||
|
|
||
| if (tossError.code === "USER_CANCEL") { | ||
| setToastMessage("결제를 취소하였습니다."); | ||
| } else { | ||
| console.error("결제 창 호출 중 오류:", tossError.message || error); | ||
| setToastMessage("결제 진행 중 오류가 발생했습니다."); | ||
| } | ||
| } finally { | ||
| setIsLoading(false); | ||
| setIsModalOpen(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Add a re-entrancy guard to handleConfirmPurchase.
Nothing prevents handleConfirmPurchase from being invoked again while a purchase is already in flight — ModalCard's primary button isn't disabled based on isLoading (only its label text changes), and the handler itself doesn't check isLoading before starting. A rapid double-click before the first click's render commits can trigger two concurrent preparePurchase() + payment.requestPayment() calls, risking duplicate payment windows/charges.
🐛 Proposed fix
const handleConfirmPurchase = async () => {
+ if (isLoading) return;
setIsLoading(true);
try {Also applies to: 100-111
🤖 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 `@jobdri/src/components/common/cards/CreditCard.tsx` around lines 55 - 90,
Update handleConfirmPurchase to return immediately when isLoading is already
true, before setting loading state or calling preparePurchase, preventing
concurrent purchase requests from rapid repeated invocation. Preserve the
existing purchase flow and cleanup behavior for the first invocation.
🔗 관련 이슈
JDDEV-73
📝 개요
⌨️ 작업 상세 내용
💡 코드 설명 및 참고사항
ModalCard.tsx로 통합하였습니다.CreditCard.tsx로 옮겼습니다.📸 스크린샷 (UI 변경 시)
https://github.com/user-attachments/assets/fbea7f9e-0f3f-4dd6-94b4-b90fb3c35fe2
결제 완료
🔍 리뷰 요구사항 (Reviewers)
Summary by CodeRabbit
New Features
UI Improvements