diff --git a/jobdri/package.json b/jobdri/package.json index f1ed1f4..c9fab17 100644 --- a/jobdri/package.json +++ b/jobdri/package.json @@ -11,6 +11,7 @@ "preinstall": "npx only-allow pnpm" }, "dependencies": { + "@tosspayments/tosspayments-sdk": "^2.7.0", "clsx": "^2.1.1", "next": "16.2.4", "react": "19.2.4", diff --git a/jobdri/pnpm-lock.yaml b/jobdri/pnpm-lock.yaml index 7475128..2557ff6 100644 --- a/jobdri/pnpm-lock.yaml +++ b/jobdri/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@tosspayments/tosspayments-sdk': + specifier: ^2.7.0 + version: 2.7.0 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -709,105 +712,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -874,28 +861,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.4': resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.4': resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.4': resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.4': resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} @@ -1047,28 +1030,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.4': resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.4': resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.4': resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.4': resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} @@ -1101,6 +1080,9 @@ packages: '@tailwindcss/postcss@4.2.4': resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} + '@tosspayments/tosspayments-sdk@2.7.0': + resolution: {integrity: sha512-qpVIMpxdmKNOKwOZuSbkyGO5CpbF3+kqJcHLjGsTdyOsNex8ai+ceQc7zyCJhq+qZf9jglJpx9Q0bGVWwGjcbQ==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1222,49 +1204,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2115,28 +2089,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3861,6 +3831,8 @@ snapshots: postcss: 8.5.12 tailwindcss: 4.2.4 + '@tosspayments/tosspayments-sdk@2.7.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/jobdri/src/app/credit/page.tsx b/jobdri/src/app/credit/page.tsx index 55d1fcf..e9ac191 100644 --- a/jobdri/src/app/credit/page.tsx +++ b/jobdri/src/app/credit/page.tsx @@ -1,13 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; import { CreditCard } from "@/components/common/cards"; -import Useage from "@/components/credit/Useage"; +import Useage from "@/components/common/credit/Useage"; +import { + fetchCreditPlans, + confirmPurchase, + type CreditPlan, +} from "@/lib/api/credit"; + +function calcDiscountRate(plan: CreditPlan, basePricePerUnit: number): string { + const original = basePricePerUnit * plan.creditAmount; + if (original <= plan.price) return ""; + const rate = Math.round(((original - plan.price) / original) * 100); + return `${rate}%`; +} export default function CreditPage() { + const [plans, setPlans] = useState([]); + const searchParams = useSearchParams(); + + useEffect(() => { + fetchCreditPlans() + .then(setPlans) + .catch(() => {}); + }, []); + + useEffect(() => { + const paymentKey = searchParams.get("paymentKey"); + const orderId = searchParams.get("orderId"); + const amount = searchParams.get("amount"); + + if (paymentKey && orderId && amount) { + confirmPurchase(paymentKey, orderId, Number(amount)).catch(() => {}); + } + }, [searchParams]); + + const basePricePerUnit = + plans.find((p) => p.planCode === "ONE_TIME")?.price ?? 2500; + return (
- - - + {plans.map((plan) => ( + + ))}
diff --git a/jobdri/src/components/common/cards/CreditCard.tsx b/jobdri/src/components/common/cards/CreditCard.tsx index 425ff42..473cafe 100644 --- a/jobdri/src/components/common/cards/CreditCard.tsx +++ b/jobdri/src/components/common/cards/CreditCard.tsx @@ -5,6 +5,7 @@ import type { HTMLAttributes } from "react"; import clsx from "clsx"; import { Button } from "@/components/common/buttons"; import ModalPurchase from "@/components/common/modal/ModalPurchase"; +import type { PlanCode } from "@/lib/api/credit"; interface CreditCardProps extends HTMLAttributes { creditCount?: number; @@ -14,6 +15,7 @@ interface CreditCardProps extends HTMLAttributes { discountRate?: string; discountLabel?: string; buttonLabel?: string; + planCode: PlanCode; onPurchase?: () => void; } @@ -25,6 +27,7 @@ export default function CreditCard({ discountRate = "21%", discountLabel = "할인", buttonLabel = "구매하기", + planCode, onPurchase, className, ...articleProps @@ -35,31 +38,26 @@ export default function CreditCard({ setIsModalOpen(true); }; - const handleConfirm = () => { - setIsModalOpen(false); - onPurchase?.(); - }; - return ( <> {isModalOpen && ( setIsModalOpen(false)} title="크레딧을 충전할까요?" /> )}
-
-
+
+
{creditCount} @@ -79,7 +77,9 @@ export default function CreditCard({
-
+
{discountRate} @@ -94,7 +94,7 @@ export default function CreditCard({ label={buttonLabel} size="large" styleType="secondary" - className="h-[46px] w-full px-4" + className="mt-auto h-[46px] items-end w-full px-4" onClick={handlePurchaseClick} />
diff --git a/jobdri/src/components/common/credit/Useage.tsx b/jobdri/src/components/common/credit/Useage.tsx new file mode 100644 index 0000000..2d2d286 --- /dev/null +++ b/jobdri/src/components/common/credit/Useage.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { CreditTable } from "."; +import { + fetchCreditTransactions, + type CreditTransaction, + type TransactionType, +} from "@/lib/api/credit"; + +const typeLabel: Record = { + CHARGE: "충전", + USE: "사용", + REFUND: "환불", + COUPON: "쿠폰", +}; + +function formatAmount(type: TransactionType, amount: number) { + const sign = type === "USE" ? "-" : "+"; + return `${sign}${amount.toLocaleString()}회`; +} + +function formatDateTime(iso: string) { + const d = new Date(iso); + return d.toLocaleString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function toRowData(tx: CreditTransaction) { + return { + id: String(tx.transactionId), + dateTime: formatDateTime(tx.createdAt), + typeLabel: typeLabel[tx.type], + content: tx.description, + amount: formatAmount(tx.type, tx.amount), + balance: `${tx.balanceAfter.toLocaleString()}회`, + }; +} + +export default function Useage() { + const [rows, setRows] = useState[]>([]); + + useEffect(() => { + fetchCreditTransactions() + .then((txs) => setRows(txs.map(toRowData))) + .catch(() => {}); + }, []); + + return ( +
+

이용 내역

+ +
+ ); +} diff --git a/jobdri/src/components/common/lnb/Lnb.tsx b/jobdri/src/components/common/lnb/Lnb.tsx index 9041ed7..db1031f 100644 --- a/jobdri/src/components/common/lnb/Lnb.tsx +++ b/jobdri/src/components/common/lnb/Lnb.tsx @@ -1,19 +1,19 @@ "use client"; -import { useState, useSyncExternalStore } from "react"; +import { useState, useEffect, useSyncExternalStore } from "react"; import { useRouter } from "next/navigation"; import { createPortal } from "react-dom"; import Icon, { type IconType } from "@/components/common/icons/Icon"; import { ModalNotice } from "@/components/common/modal"; import { AUTH_STORAGE_KEYS, getStoredAuthEmail } from "@/lib/auth"; import Logo from "@/assets/ic_LOGO_minimum.svg"; +import { fetchCreditBalance } from "@/lib/api/credit"; type LnbItemKey = "experience" | "apply"; interface LnbProps { initialActiveItem?: LnbItemKey; email?: string; - creditCount?: number; } interface LnbNavItem { @@ -64,11 +64,7 @@ function getEmailInitial(email: string) { return email.trim().charAt(0).toUpperCase() || "J"; } -export default function Lnb({ - initialActiveItem, - email, - creditCount = 32, -}: LnbProps) { +export default function Lnb({ initialActiveItem, email }: LnbProps) { const router = useRouter(); const storedEmail = useSyncExternalStore( subscribeToStoredEmail, @@ -77,6 +73,7 @@ export default function Lnb({ ); const displayEmail = (email ?? storedEmail) || defaultEmail; const emailInitial = getEmailInitial(displayEmail); + const [creditCount, setCreditCount] = useState(0); const [isFold, setIsFold] = useState(false); const [showComingSoonModal, setShowComingSoonModal] = useState(false); const [activeItem, setActiveItem] = useState( @@ -95,6 +92,12 @@ export default function Lnb({ const closeComingSoonModal = () => setShowComingSoonModal(false); + useEffect(() => { + fetchCreditBalance() + .then(setCreditCount) + .catch(() => {}); + }, []); + return ( <>