-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: credit fix [JDDEV-73] #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
79a0fc8
0e54135
972cf24
35e0c69
e6859b8
bf7dcb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,20 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { useState, useEffect } from "react"; | ||
| import type { HTMLAttributes } from "react"; | ||
| import clsx from "clsx"; | ||
| import { Button } from "@/components/common/buttons"; | ||
| import ModalPurchase from "@/components/common/modal/ModalPurchase"; | ||
| import { ModalCard } from "../modal/ModalCard"; | ||
| import { ModalOverlay } from "../modal/ModalOverlay"; | ||
| import type { PlanCode } from "@/lib/api/credit"; | ||
| import { preparePurchase } from "@/lib/api/credit"; | ||
| import { loadTossPayments, ANONYMOUS } from "@tosspayments/tosspayments-sdk"; | ||
| import { Toast } from "../toast"; | ||
|
|
||
| interface CreditCardProps extends HTMLAttributes<HTMLElement> { | ||
| creditCount?: number; | ||
| creditLabel?: string; | ||
| price?: string; | ||
| currencyLabel?: string; | ||
| discountRate?: string; | ||
| discountLabel?: string; | ||
| buttonLabel?: string; | ||
| planCode: PlanCode; | ||
| onPurchase?: () => void; | ||
|
|
@@ -27,38 +28,98 @@ export default function CreditCard({ | |
| planCode, | ||
| onPurchase, | ||
| className, | ||
| discountLabel, | ||
| creditLabel, | ||
| currencyLabel, | ||
| ...articleProps | ||
| }: CreditCardProps) { | ||
| const [isModalOpen, setIsModalOpen] = useState(false); | ||
| const [isLoading, setIsLoading] = useState(false); | ||
| const [toastMessage, setToastMessage] = useState<string | null>(null); | ||
|
|
||
| const handlePurchaseClick = () => { | ||
| onPurchase?.(); | ||
| setIsModalOpen(true); | ||
| }; | ||
|
|
||
| const handleCloseModal = () => { | ||
| if (!isLoading) setIsModalOpen(false); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (toastMessage) { | ||
| const timer = setTimeout(() => { | ||
| setToastMessage(null); | ||
| }, 3000); | ||
| return () => clearTimeout(timer); | ||
| } | ||
| }, [toastMessage]); | ||
|
|
||
| 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); | ||
| } | ||
| }; | ||
|
Comment on lines
+55
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐๏ธ Data Integrity & Integration | ๐ Major | โก Quick win Add a re-entrancy guard to Nothing prevents ๐ Proposed fix const handleConfirmPurchase = async () => {
+ if (isLoading) return;
setIsLoading(true);
try {Also applies to: 100-111 ๐ค Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <> | ||
| {toastMessage && ( | ||
| <div className="fixed top-10 left-1/2 z-[9999] -translate-x-1/2"> | ||
| <Toast message={toastMessage} position="top" /> | ||
| </div> | ||
| )} | ||
|
|
||
| {isModalOpen && ( | ||
| <ModalPurchase | ||
| creditCount={creditCount} | ||
| price={price} | ||
| planCode={planCode} | ||
| onClose={() => setIsModalOpen(false)} | ||
| title="ํฌ๋ ๋ง์ ์ถฉ์ ํ ๊น์?" | ||
| /> | ||
| <ModalOverlay onClose={handleCloseModal}> | ||
| <ModalCard | ||
| onSecondaryClick={handleCloseModal} | ||
| onPrimaryClick={handleConfirmPurchase} | ||
| title="ํฌ๋ ๋ง์ ์ถฉ์ ํ ๊น์?" | ||
| description={`${creditCount} ํฌ๋ ๋ง์ ์ถฉ์ ํฉ๋๋ค.`} | ||
| secondaryBtn="๋ซ๊ธฐ" | ||
| primaryBtn={isLoading ? "์ฒ๋ฆฌ ์ค..." : "์ถฉ์ ํ๊ธฐ"} | ||
| /> | ||
| </ModalOverlay> | ||
| )} | ||
| <article | ||
|
|
||
| {/* ํฌ๋ ๋ง ์นด๋ UI */} | ||
| <main | ||
| className={clsx( | ||
| "flex w-full flex-col items-center h-fit rounded-card bg-bg-contents-default px-8 pt-8 pb-7 shadow-card", | ||
| className, | ||
| )} | ||
| {...articleProps} | ||
| > | ||
| <div className="flex flex-1 flex-col justify-between gap-8 w-full"> | ||
| <div className="flex flex-col items-center gap-4"> | ||
| <div className="flex flex-col items-center gap-4"> | ||
| <div className="flex items-end justify-center gap-1"> | ||
| <span className="text-center text-[32px] font-bold leading-[130%] text-text-neutral-title "> | ||
| {creditCount} | ||
|
|
@@ -100,7 +161,7 @@ export default function CreditCard({ | |
| </p> | ||
| </div> | ||
| </div> | ||
| </article> | ||
| </main> | ||
| </> | ||
| ); | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +0,0 @@ | ||
| export { default as ModalInput } from "./ModalInput"; | ||
| export { default as ModalFileUpload } from "./ModalFileUpload"; | ||
| export { default as ModalLinkInputDemo } from "./ModalLinkInputDemo"; | ||
| export { default as ModalNotice } from "./ModalNotice"; | ||
| export { default as ModalPurchase } from "./ModalPurchase"; | ||
| export { default as ModalAdd } from "./ModalAdd"; | ||
There was a problem hiding this comment.
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
confirmPurchasecalls.The stated design ("clears them before confirmation") isn't what the code does:
confirmPurchaseruns first, andwindow.history.replaceStateonly clearspaymentKey/orderId/amountafterward. 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 andprocessConfirmwill run again with the samepaymentKey, sending a duplicate confirmation request for the same payment.๐ Proposed fix
๐ค Prompt for AI Agents