Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jobdri/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@portone/browser-sdk": "^0.1.9",
"@tanstack/react-query": "^5.101.2",
"@tosspayments/tosspayments-sdk": "^2.7.0",
"clsx": "^2.1.1",
Expand Down
8 changes: 8 additions & 0 deletions jobdri/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 41 additions & 9 deletions jobdri/src/app/credit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useState, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { useSearchParams, useRouter } from "next/navigation";
import { CreditCard } from "@/components/common/cards";
import Useage from "@/components/common/credit/Useage";
import {
Expand All @@ -11,6 +11,7 @@ import {
} from "@/lib/api/credit";
import Lnb from "@/components/common/lnb/Lnb";
import PageHeader from "@/components/common/PageHeader";
import { BusinessFooter } from "@/components/common/footer";

function calcDiscountRate(plan: CreditPlan, basePricePerUnit: number): string {
const original = basePricePerUnit * plan.creditAmount;
Expand All @@ -21,7 +22,9 @@ function calcDiscountRate(plan: CreditPlan, basePricePerUnit: number): string {

function CreditContent() {
const [plans, setPlans] = useState<CreditPlan[]>([]);
const [isConfirming, setIsConfirming] = useState(false); // 승인 중 로딩 상태 추가
const searchParams = useSearchParams();
const router = useRouter(); // 라우터 훅 추가

useEffect(() => {
fetchCreditPlans()
Expand All @@ -34,17 +37,43 @@ function CreditContent() {
const orderId = searchParams.get("orderId");
const amount = searchParams.get("amount");

// URL에 결제 정보가 있으면 승인 로직 시작
if (paymentKey && orderId && amount) {
confirmPurchase(paymentKey, orderId, Number(amount)).catch(() => {});
const processConfirm = async () => {
setIsConfirming(true); // 화면 잠금 (중복 요청 방지)
try {
await confirmPurchase(paymentKey, orderId, Number(amount));

alert("크레딧 충전이 완료되었습니다!");
} catch (error) {
console.error("결제 승인 실패:", error);
alert("결제 승인에 실패했습니다. 다시 시도해 주세요.");
} finally {
setIsConfirming(false);
// 새로고침 시 중복 호출 방지
router.replace("/credit");
Comment on lines +47 to +54

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'page\.tsx$' jobdri 2>/dev/null | rg '/credit/page\.tsx|credit/page\.tsx' || true

echo "== file outline =="
ast-grep outline jobdri/src/app/credit/page.tsx --view expanded || true

echo "== relevant lines =="
sed -n '1,180p' jobdri/src/app/credit/page.tsx | nl -ba

echo "== related payment confirm usages =="
rg -n "confirmPurchase|paymentKey|orderId|processConfirm|setIsConfirming|router\.replace\(\"/credit\"\)" jobdri/src/app/credit -S || true

Repository: JobDri-Developer/FrontEnd

Length of output: 501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git status/diff summary =="
git diff --stat || true

echo "== focused diff for credit page =="
git diff -- jobdri/src/app/credit/page.tsx | sed -n '1,220p' || true

Repository: JobDri-Developer/FrontEnd

Length of output: 228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== credit page content =="
sed -n '1,180p' jobdri/src/app/credit/page.tsx | awk '{ printf "%5d\t%s\n", NR, $0 }'

echo "== payment-related searches in jobdri/src =="
rg -n "confirmPurchase|paymentKey|orderId|processConfirm|setIsConfirming|router\.replace\(\"/credit\"\)|토스페이먼츠|tosspayments" jobdri/src/app -S || true

Repository: JobDri-Developer/FrontEnd

Length of output: 5184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== credit api file candidates =="
fd -a 'credit\.(ts|tsx|js)$|credit' jobdri/src/lib | sort | sed -n '1,80p'

echo "== confirmPurchase definitions =="
rg -n "function confirmPurchase|const confirmPurchase|export .*confirmPurchase|export async function confirmPurchase|confirmPurchase\s*=" jobdri/src -S || true

echo "== relevant API source snippets =="
while IFS= read -r f; do
  echo "--- $f"
  grep -n "confirmPurchase\|paymentKey\|orderId\|amount" "$f" | sed -n '1,120p' || true
done < <(rg -l "confirmPurchase" jobdri/src -S)

echo "== read credit api top sections =="
while IFS= read -r f; do
  echo "--- $f"
  sed -n '1,220p' "$f" | awk '{ printf "%5d\t%s\n", NR, $0 }'
done < <(rg -l "confirmPurchase" jobdri/src -S)

Repository: JobDri-Developer/FrontEnd

Length of output: 438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== credit api top sections =="
sed -n '1,180p' jobdri/src/lib/api/credit.ts | awk '{ printf "%5d\t%s\n", NR, $0 }'

echo "== related backend/reconciliation API candidates =="
rg -n "confirmPurchase|order\\.?id|payment\\.?key|credit|충전|충전내역|usage|내역" . -S --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,240p' || true

Repository: JobDri-Developer/FrontEnd

Length of output: 8792


Preserve payment callback data before redirecting.

router.replace("/credit") is called in finally, so any failure in confirmPurchase(paymentKey, orderId, amount) redirects to /credit without the authenticated payment callback parameters. A transient confirmation failure then leaves the user unable to retry or reconcile the payment. Call /credit successfully before redirecting, and allow idempotent retry/recovery using the same parameter set on failure.

🤖 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 47 - 54, Update the confirmation
flow around confirmPurchase and the finally block so paymentKey, orderId, and
amount are preserved when confirmation fails; only redirect with
router.replace("/credit") after successful confirmation, while retaining the
callback parameters and enabling an idempotent retry or recovery path for
failures.

}
};

processConfirm();
}
}, [searchParams]);
}, [searchParams, router]);

const basePricePerUnit =
plans.find((p) => p.planCode === "ONE_TIME")?.price ?? 2500;

return (
<>
<PageHeader />

{/* 결제 승인 중일 때 화면을 덮는 로딩 UI */}
{isConfirming && (
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="text-white text-h24-bold animate-pulse">
결제를 안전하게 승인하고 있습니다...
</div>
</div>
)}
<section className="flex flex-row gap-4 w-full mt-8 mb-16 mx-auto">
{plans.map((plan) => (
<CreditCard
Expand All @@ -68,13 +97,16 @@ function CreditContent() {

export default function CreditPage() {
return (
<div className="flex min-h-screen w-full bg-[#F5F6F9] overflow-x-hidden ">
<div className="flex min-h-screen w-full bg-[#F5F6F9]">
<Lnb />
<main className="flex-1 w-full max-w-[1320px] min-w-[1060px] px-18 pt-12 pb-60 mx-auto">
<Suspense fallback={<div>로딩 중...</div>}>
<CreditContent />
</Suspense>
</main>
<div className="w-full flex-1">
<main className="flex-1 w-full max-w-[1320px] min-w-[1060px] px-18 pt-12 pb-60 mx-auto overflow-x-hidden">
<Suspense fallback={<div>로딩 중...</div>}>
<CreditContent />
</Suspense>
</main>
<BusinessFooter />
</div>
</div>
);
}
76 changes: 76 additions & 0 deletions jobdri/src/app/policy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";

import { Suspense } from "react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import Lnb from "@/components/common/lnb/Lnb";
import { TERMS_TEXT, PRIVACY_TEXT } from "@/constants/policy";

function PolicyContent() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();

const tabParam = searchParams.get("tab");
const activeTab = tabParam === "privacy" ? "privacy" : "terms"; // 기본값은 terms

const handleTabClick = (tab: "terms" | "privacy") => {
const params = new URLSearchParams(searchParams.toString());
params.set("tab", tab);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
};

return (
<main className="flex-1 w-full max-w-[800px] px-8 pt-16 pb-32 mx-auto">
<h1 className="text-3xl font-bold text-text-neutral-title mb-10">
서비스 정책
</h1>

<div className="flex gap-6 mb-8 border-b border-gray-200">
<button
className={`pb-3 px-2 text-[18px] font-medium border-b-2 transition-colors ${
activeTab === "terms"
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-400 hover:text-gray-600"
}`}
onClick={() => handleTabClick("terms")}
>
이용약관
</button>
<button
className={`pb-3 px-2 text-[18px] font-medium border-b-2 transition-colors ${
activeTab === "privacy"
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-400 hover:text-gray-600"
}`}
onClick={() => handleTabClick("privacy")}
>
개인정보 처리방침
</button>
</div>

<div className="bg-white rounded-card shadow-sm p-8 min-h-[600px]">
<div
className="text-gray-700 whitespace-pre-wrap leading-[1.8] text-[15px]"
dangerouslySetInnerHTML={{
__html: activeTab === "terms" ? TERMS_TEXT : PRIVACY_TEXT,
}}
/>
</div>
</main>
);
}

export default function PolicyPage() {
return (
<div className="flex min-h-screen w-full bg-[#F5F6F9]">
<Lnb />
<Suspense
fallback={
<div className="flex-1 w-full pt-16 text-center">로딩 중...</div>
}
>
<PolicyContent />
</Suspense>
</div>
);
}
17 changes: 11 additions & 6 deletions jobdri/src/components/common/cards/CreditCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,17 @@ export default function CreditCard({
</div>
</div>

<Button
label={buttonLabel}
size="large"
styleType="secondary"
onClick={handlePurchaseClick}
/>
<div className="flex flex-1 flex-col justify-between gap-2 w-full">
<Button
label={buttonLabel}
size="large"
styleType="secondary"
onClick={handlePurchaseClick}
/>
<p className="text-cap12-med text-text-neutral-caption items-center justify-center flex">
* 구매한 이용권은 구매일로부터 6개월간 유효합니다
</p>
</div>
</div>
</article>
</>
Expand Down
18 changes: 10 additions & 8 deletions jobdri/src/components/common/footer/BusinessFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { HTMLAttributes } from "react";
import Link from "next/link";
import clsx from "clsx";
import Logo from "@/assets/ic_LOGO_minimum.svg";

Expand Down Expand Up @@ -67,19 +68,20 @@ export default function BusinessFooter({
</p>

<div className="flex h-4 items-center gap-4">
<button
type="button"
className="flex h-4 items-center text-cap12-med text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]"
{/* 💡 button을 Link로 변경하고 href와 hover 효과 추가 */}
<Link
href="/policy?tab=terms"
className="flex h-4 items-center text-cap12-med text-gray-400 transition-colors hover:text-gray-600 [font-feature-settings:'liga'_off,'clig'_off]"
>
이용 약관
</button>
</Link>
<span className="h-4 w-px bg-[#D3D5D7]" />
<button
type="button"
className="flex h-4 items-center text-cap12-med text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]"
<Link
href="/policy?tab=privacy"
className="flex h-4 items-center text-cap12-med text-gray-400 transition-colors hover:text-gray-600 [font-feature-settings:'liga'_off,'clig'_off]"
>
개인정보 처리방침
</button>
</Link>
</div>
</div>
</div>
Expand Down
Loading