diff --git a/jobdri/src/app/credit/page.tsx b/jobdri/src/app/credit/page.tsx index d1129cd..f9740ed 100644 --- a/jobdri/src/app/credit/page.tsx +++ b/jobdri/src/app/credit/page.tsx @@ -37,27 +37,30 @@ function CreditContent() { const orderId = searchParams.get("orderId"); const amount = searchParams.get("amount"); - // URL에 결제 정보가 있으면 승인 로직 시작 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]); const basePricePerUnit = plans.find((p) => p.planCode === "ONE_TIME")?.price ?? 2500; @@ -82,9 +85,6 @@ function CreditContent() { price={plan.price.toLocaleString()} planCode={plan.planCode} discountRate={calcDiscountRate(plan, basePricePerUnit)} - discountLabel={ - calcDiscountRate(plan, basePricePerUnit) ? "할인" : "" - } /> ))} diff --git a/jobdri/src/components/common/cards/CreditCard.tsx b/jobdri/src/components/common/cards/CreditCard.tsx index a9b0e00..08ddb57 100644 --- a/jobdri/src/components/common/cards/CreditCard.tsx +++ b/jobdri/src/components/common/cards/CreditCard.tsx @@ -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 { creditCount?: number; - creditLabel?: string; price?: string; - currencyLabel?: string; discountRate?: string; - discountLabel?: string; buttonLabel?: string; planCode: PlanCode; onPurchase?: () => void; @@ -27,30 +28,90 @@ 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(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); + } + }; + return ( <> + {toastMessage && ( +
+ +
+ )} + {isModalOpen && ( - setIsModalOpen(false)} - title="크레딧을 충전할까요?" - /> + + + )} -
-
+
{creditCount} @@ -100,7 +161,7 @@ export default function CreditCard({

-
+ ); } diff --git a/jobdri/src/components/common/credit/CreditTable.tsx b/jobdri/src/components/common/credit/CreditTable.tsx index 4e81a74..c939f04 100644 --- a/jobdri/src/components/common/credit/CreditTable.tsx +++ b/jobdri/src/components/common/credit/CreditTable.tsx @@ -22,7 +22,7 @@ const defaultRows: CreditRowData[] = Array.from({ length: 6 }, (_, i) => ({ export default function CreditTable({ rows = defaultRows }: CreditTableProps) { return (
-
+
{rows.map((row, index) => ( { window.removeEventListener( diff --git a/jobdri/src/components/common/modal/ModalCard.tsx b/jobdri/src/components/common/modal/ModalCard.tsx index 91b8a4a..3f1fa4c 100644 --- a/jobdri/src/components/common/modal/ModalCard.tsx +++ b/jobdri/src/components/common/modal/ModalCard.tsx @@ -23,7 +23,7 @@ export const ModalCard: React.FC = ({ onErrorClick, }) => { return ( -
+

{title} diff --git a/jobdri/src/components/common/modal/ModalOverlay.tsx b/jobdri/src/components/common/modal/ModalOverlay.tsx index cf62ec4..96890ca 100644 --- a/jobdri/src/components/common/modal/ModalOverlay.tsx +++ b/jobdri/src/components/common/modal/ModalOverlay.tsx @@ -4,9 +4,10 @@ import { useEffect, type ReactNode } from "react"; interface ModalOverlayProps { children: ReactNode; + onClose?: () => void; } -export function ModalOverlay({ children }: ModalOverlayProps) { +export function ModalOverlay({ children, onClose }: ModalOverlayProps) { useEffect(() => { document.body.style.overflow = "hidden"; return () => { @@ -15,7 +16,10 @@ export function ModalOverlay({ children }: ModalOverlayProps) { }, []); return ( -
+
e.stopPropagation()}> {children}
diff --git a/jobdri/src/components/common/modal/ModalPurchase.tsx b/jobdri/src/components/common/modal/ModalPurchase.tsx deleted file mode 100644 index e26166d..0000000 --- a/jobdri/src/components/common/modal/ModalPurchase.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import { useRef, useState } from "react"; -import clsx from "clsx"; -import { loadTossPayments, ANONYMOUS } from "@tosspayments/tosspayments-sdk"; -import Icon from "@/components/common/icons/Icon"; -import { ButtonCtaModal } from "@/components/common/buttons"; -import useOutsideClick from "@/hooks/useOutsideClick"; -import { preparePurchase, type PlanCode } from "@/lib/api/credit"; -import { Toast } from "../toast"; - -interface ModalPurchaseProps { - creditCount?: number; - price?: string; - planCode: PlanCode; - onClose?: () => void; - className?: string; - title?: string; -} - -export default function ModalPurchase({ - creditCount = 3, - price = "2,500", - planCode, - onClose, - className, - title, -}: ModalPurchaseProps) { - const modalRef = useRef(null); - const [isLoading, setIsLoading] = useState(false); - const [toastMessage, setToastMessage] = useState(null); - - useOutsideClick(modalRef, onClose); - - const handleConfirm = 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); - } - }; - - return ( - <> - {toastMessage && ( -
- -
- )} -
-
- {/* 헤더 */} -
- -
- - {/* 바디 */} -
- {/* 텍스트 */} -
- - {title} - - - {creditCount}크레딧 | {price}원 - -
- - {/* 버튼 */} - -
-
-
- - ); -} diff --git a/jobdri/src/components/common/modal/index.ts b/jobdri/src/components/common/modal/index.ts index 4f6797b..e69de29 100644 --- a/jobdri/src/components/common/modal/index.ts +++ b/jobdri/src/components/common/modal/index.ts @@ -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";