From 79a0fc8da073ee8b21e29a9824b6aa92afd659d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Mon, 27 Jul 2026 18:19:57 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Feat:=20=ED=81=AC=EB=A0=88=EB=94=A7=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EC=84=B1=EA=B3=B5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/app/credit/page.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/jobdri/src/app/credit/page.tsx b/jobdri/src/app/credit/page.tsx index d1129cd..ff89f10 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; From 0e5413524438b129d12c34168ef84a1d304152a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Mon, 27 Jul 2026 18:38:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Feat:=20=EC=9D=B4=EC=A4=91=20=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=ED=81=AC=20=EC=82=AD=EC=A0=9C=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/components/common/credit/CreditTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) => ( Date: Mon, 27 Jul 2026 18:38:36 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Feat:=20=EA=B5=AC=EB=A7=A4=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=88=98=EC=A0=95=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/cards/CreditCard.tsx | 95 ++++++++++++++++--- .../src/components/common/modal/ModalCard.tsx | 2 +- .../components/common/modal/ModalOverlay.tsx | 8 +- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/jobdri/src/components/common/cards/CreditCard.tsx b/jobdri/src/components/common/cards/CreditCard.tsx index a9b0e00..e849c86 100644 --- a/jobdri/src/components/common/cards/CreditCard.tsx +++ b/jobdri/src/components/common/cards/CreditCard.tsx @@ -1,11 +1,15 @@ "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; @@ -27,30 +31,91 @@ 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 +165,7 @@ export default function CreditCard({

-
+ ); } 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}
From 35e0c69dff6948a51ff63830cc2e972c71853e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Mon, 27 Jul 2026 18:40:07 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Refactor:=20=EA=B5=AC=EB=A7=A4=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/modal/ModalPurchase.tsx | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 jobdri/src/components/common/modal/ModalPurchase.tsx 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}원 - -
- - {/* 버튼 */} - -
-
-
- - ); -} From e6859b83266ba90aaab43308b1d3d06dd9a88b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Mon, 27 Jul 2026 19:55:25 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Fix:=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/components/common/cards/CreditCard.tsx | 4 ---- jobdri/src/components/common/lnb/Lnb.tsx | 7 ++----- jobdri/src/components/common/modal/index.ts | 6 ------ 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/jobdri/src/components/common/cards/CreditCard.tsx b/jobdri/src/components/common/cards/CreditCard.tsx index e849c86..08ddb57 100644 --- a/jobdri/src/components/common/cards/CreditCard.tsx +++ b/jobdri/src/components/common/cards/CreditCard.tsx @@ -13,11 +13,8 @@ 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; @@ -31,7 +28,6 @@ export default function CreditCard({ planCode, onPurchase, className, - ...articleProps }: CreditCardProps) { const [isModalOpen, setIsModalOpen] = useState(false); diff --git a/jobdri/src/components/common/lnb/Lnb.tsx b/jobdri/src/components/common/lnb/Lnb.tsx index eefb651..6d13134 100644 --- a/jobdri/src/components/common/lnb/Lnb.tsx +++ b/jobdri/src/components/common/lnb/Lnb.tsx @@ -10,7 +10,7 @@ import { import { createPortal } from "react-dom"; import clsx from "clsx"; import { useRouter } from "next/navigation"; -import { ModalNotice } from "@/components/common/modal"; +import ModalNotice from "../modal/ModalNotice"; import { Toast, type ToastVariant } from "@/components/common/toast"; import { AUTH_STORAGE_KEYS, @@ -281,10 +281,7 @@ export default function Lnb({ ); }; - window.addEventListener( - MOCK_APPLY_DELETED_EVENT, - handleMockApplyDeleted, - ); + window.addEventListener(MOCK_APPLY_DELETED_EVENT, handleMockApplyDeleted); return () => { window.removeEventListener( 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"; From bf7dcb1ea7b6736d930bbb134fea72d1393f9547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Mon, 27 Jul 2026 20:04:02 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Fix:=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20[JDDEV-73]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/app/credit/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/jobdri/src/app/credit/page.tsx b/jobdri/src/app/credit/page.tsx index ff89f10..f9740ed 100644 --- a/jobdri/src/app/credit/page.tsx +++ b/jobdri/src/app/credit/page.tsx @@ -85,9 +85,6 @@ function CreditContent() { price={plan.price.toLocaleString()} planCode={plan.planCode} discountRate={calcDiscountRate(plan, basePricePerUnit)} - discountLabel={ - calcDiscountRate(plan, basePricePerUnit) ? "할인" : "" - } /> ))}