Skip to content

Feat: credit fix [JDDEV-73] - #109

Merged
yiyoonseo merged 6 commits into
developfrom
feature/JDDEV-73-credit-fix
Jul 27, 2026
Merged

Feat: credit fix [JDDEV-73]#109
yiyoonseo merged 6 commits into
developfrom
feature/JDDEV-73-credit-fix

Conversation

@yiyoonseo

@yiyoonseo yiyoonseo commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

JDDEV-73

  • Close #

📝 개요

  • 크레딧 관련 수정 작업 진행했습니다.

⌨️ 작업 상세 내용

  • 결제 완료 후 로직 보강
  • 크레딧 페이지 QA 사항 반영

💡 코드 설명 및 참고사항

  • 결제 모달을 ModalCard.tsx로 통합하였습니다.
  • 결제 관련 로직을 CreditCard.tsx로 옮겼습니다.
  • 일부 필요없는 타입을 삭제하였습니다.
  • 중복 border를 삭제하였습니다.

📸 스크린샷 (UI 변경 시)

image 모달 변경 image 이중 스트로크 삭제

https://github.com/user-attachments/assets/fbea7f9e-0f3f-4dd6-94b4-b90fb3c35fe2
결제 완료

🔍 리뷰 요구사항 (Reviewers)

  • [ ]

⚠️ 로컬 실행 시 유의사항

Summary by CodeRabbit

  • New Features

    • Added a streamlined credit purchase flow with payment approval, loading states, and completion feedback.
    • Added click-outside behavior for closing purchase modals.
    • Improved handling of canceled or failed payments, preventing duplicate payment attempts.
  • UI Improvements

    • Updated purchase modal sizing and credit table styling.
    • Refined credit card purchase controls and feedback messages.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The credit purchase flow now uses a confirmation modal in CreditCard, submits payments through Toss Payments, and handles returned payment parameters in CreditContent with query cleanup, result alerts, and page reloads. Shared modal behavior and related layout styling were also updated.

Changes

Credit purchase flow

Layer / File(s) Summary
Purchase modal and shared modal behavior
jobdri/src/components/common/cards/CreditCard.tsx, jobdri/src/components/common/modal/*, jobdri/src/components/common/lnb/Lnb.tsx
Credit purchases use ModalOverlay and ModalCard; overlay clicks can close the modal, modal sizing changed, and ModalNotice uses a direct default import.
Credit card payment submission
jobdri/src/components/common/cards/CreditCard.tsx, jobdri/src/app/credit/page.tsx
CreditCard prepares purchases and requests Toss Payments with loading, cancellation, error, and toast handling; callers now provide planCode and discountRate.
Returned payment confirmation
jobdri/src/app/credit/page.tsx, jobdri/src/components/common/credit/CreditTable.tsx
Payment query parameters are cleared before confirmation, success reloads the page, failures alert the user, and the credit table’s top border styling is removed.

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

Possibly related PRs

Suggested labels: 🎨 UI, ⭐ Feature

Suggested reviewers: minnngo

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
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title is related to the credit payment changes, but "credit fix" is too vague to convey the main update. Use a specific title such as "Improve credit payment flow and consolidate purchase modal".
✅ Passed checks (3 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/JDDEV-73-credit-fix

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

🧹 Nitpick comments (1)
jobdri/src/components/common/modal/ModalOverlay.tsx (1)

10-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between b0a8a08 and bf7dcb1.

📒 Files selected for processing (8)
  • jobdri/src/app/credit/page.tsx
  • jobdri/src/components/common/cards/CreditCard.tsx
  • jobdri/src/components/common/credit/CreditTable.tsx
  • jobdri/src/components/common/lnb/Lnb.tsx
  • jobdri/src/components/common/modal/ModalCard.tsx
  • jobdri/src/components/common/modal/ModalOverlay.tsx
  • jobdri/src/components/common/modal/ModalPurchase.tsx
  • jobdri/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

Comment on lines 40 to +63
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]);

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

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.

Comment on lines +55 to +90
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);
}
};

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

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.

@yiyoonseo yiyoonseo changed the title Feature/jddev 73 credit fix Feat: credit fix [JDDEV-73] Jul 27, 2026
@yiyoonseo
yiyoonseo merged commit 0e81962 into develop Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant