Skip to content

Commit 96201f1

Browse files
authored
refactor: 거래내역 관련 수정
2 parents 1d74a44 + ff9e9aa commit 96201f1

10 files changed

Lines changed: 354 additions & 2 deletions

File tree

app/wallet/api/fetch-transactions-detail.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface TransactionDetail {
99
amount: number;
1010
displayDescription: string;
1111
createdAt: string;
12+
txHash: string;
1213
}
1314

1415
export async function fetchTransactionDetail(id: string): Promise<TransactionDetail> {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BlockchainTransaction } from "@/app/wallet/blockchain-details/types/blockchain"
2+
import {fetchWithAuth} from "@/lib/fetchWithAuth";
3+
import {getApiUrl} from "@/lib/getApiUrl";
4+
5+
const API_URL = getApiUrl();
6+
7+
export async function fetchTxDetail(txHash: string): Promise<BlockchainTransaction> {
8+
const res = await fetchWithAuth(`${API_URL}/api/users/wallet/tx/${txHash}`)
9+
10+
const data = await res.json()
11+
12+
if (!res.ok || !data.isSuccess) {
13+
throw new Error(data.message || "트랜잭션 조회 실패")
14+
}
15+
16+
return data.result as BlockchainTransaction
17+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
import { useParams } from "next/navigation"
5+
import { useToast } from "@/hooks/use-toast"
6+
import { fetchTxDetail } from "../../api/fetch-txhash-details"
7+
import BlockchainDetailsLoading from "../components/BlockchainDetailsLoading"
8+
import { HeaderSection } from "../components/HeaderSection"
9+
import {TransactionDetailCard} from "@/app/wallet/blockchain-details/components/TransactionDetailCard";
10+
import {BlockchainTransaction} from "@/app/wallet/blockchain-details/types/blockchain";
11+
import MascotCardSection from "@/app/wallet/blockchain-details/components/MascotCardSection";
12+
13+
export default function BlockchainDetailsPage() {
14+
const { txHash } = useParams()
15+
const { toast } = useToast()
16+
const [transaction, setTransaction] = useState<BlockchainTransaction | null>(null)
17+
18+
useEffect(() => {
19+
if (!txHash || typeof txHash !== "string") {
20+
toast({ title: "txHash 누락", description: "유효한 트랜잭션 해시가 필요합니다." })
21+
return
22+
}
23+
24+
const fetchData = async () => {
25+
try {
26+
const tx = await fetchTxDetail(txHash)
27+
setTransaction(tx)
28+
} catch (error: any) {
29+
toast({ title: "트랜잭션 조회 실패", description: error.message })
30+
}
31+
}
32+
33+
fetchData()
34+
}, [txHash])
35+
36+
if (!transaction) {
37+
return <BlockchainDetailsLoading />
38+
}
39+
40+
return (
41+
<div className="min-h-screen bg-[#F9FAFB] max-w-md mx-auto">
42+
<HeaderSection />
43+
<div className="p-5">
44+
<MascotCardSection />
45+
<TransactionDetailCard transaction={transaction} />
46+
</div>
47+
</div>
48+
)
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export default function BlockchainDetailsLoading() {
2+
return (
3+
<div className="min-h-screen bg-[#F9FAFB] max-w-md mx-auto">
4+
<header className="bg-white p-5 pt-8 shadow-sm">
5+
<div className="flex items-center">
6+
<div className="w-10 h-10 bg-gray-200 rounded-lg animate-pulse mr-4"></div>
7+
<div className="w-32 h-6 bg-gray-200 rounded animate-pulse"></div>
8+
</div>
9+
</header>
10+
11+
<div className="p-5">
12+
<div className="bg-white rounded-2xl p-6 shadow-sm mb-4">
13+
<div className="flex items-center gap-3">
14+
<div className="w-12 h-12 bg-gray-200 rounded-full animate-pulse"></div>
15+
<div>
16+
<div className="w-20 h-3 bg-gray-200 rounded animate-pulse mb-2"></div>
17+
<div className="w-32 h-4 bg-gray-200 rounded animate-pulse"></div>
18+
</div>
19+
</div>
20+
</div>
21+
22+
<div className="w-full h-12 bg-gray-200 rounded-lg animate-pulse mb-4"></div>
23+
24+
<div className="bg-white rounded-2xl shadow-sm overflow-hidden">
25+
{Array.from({ length: 8 }).map((_, index) => (
26+
<div
27+
key={index}
28+
className={`flex items-center justify-between px-4 py-4 border-b border-gray-100 ${
29+
index % 2 === 1 ? "bg-gray-50" : ""
30+
}`}
31+
>
32+
<div className="w-20 h-4 bg-gray-200 rounded animate-pulse"></div>
33+
<div className="w-32 h-4 bg-gray-200 rounded animate-pulse"></div>
34+
</div>
35+
))}
36+
</div>
37+
</div>
38+
</div>
39+
)
40+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ArrowLeft } from "lucide-react"
2+
import { useRouter } from "next/navigation"
3+
import { Button } from "@/components/ui/button"
4+
5+
export function HeaderSection() {
6+
const router = useRouter()
7+
8+
return (
9+
<header className="bg-white p-5 pt-8 shadow-sm">
10+
<div className="flex items-center justify-between">
11+
<div className="flex items-center">
12+
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-4">
13+
<ArrowLeft className="h-5 w-5" />
14+
</Button>
15+
<h1 className="text-lg font-semibold text-[#111827]">블록체인 상세</h1>
16+
</div>
17+
</div>
18+
</header>
19+
)
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Shield} from "lucide-react";
2+
3+
import Image from 'next/image';
4+
5+
export default function MascotCardSection() {
6+
return (
7+
<div>
8+
{/* 마스코트 보안 인증 섹션 */}
9+
<div className="bg-white rounded-2xl p-6 shadow-sm mb-4 text-center">
10+
<div className="flex justify-center mb-4">
11+
<div className="relative">
12+
<Image src="/images/bunny-mascot.png" alt="토킷 마스코트" width={80} height={80} className="mx-auto" />
13+
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
14+
<Shield className="w-4 h-4 text-white" />
15+
</div>
16+
</div>
17+
</div>
18+
<div className="mb-3">
19+
<h3 className="text-lg font-semibold text-[#111827] mb-1">거래 검증 완료</h3>
20+
<p className="text-sm text-[#6B7280]">토킷이 블록체인에서 안전하게 검증했어요</p>
21+
</div>
22+
</div>
23+
</div>
24+
)
25+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { Badge } from "@/components/ui/badge"
5+
import { CheckCircle, Clock } from "lucide-react"
6+
import { BlockchainTransaction } from "../types/blockchain"
7+
8+
interface Props {
9+
transaction: BlockchainTransaction
10+
}
11+
12+
export function TransactionDetailCard({ transaction }: Props) {
13+
const [showMoreDetails, setShowMoreDetails] = useState(false)
14+
15+
return (
16+
<div className="bg-white rounded-2xl shadow-sm overflow-hidden">
17+
{/* 거래 해시 */}
18+
<DetailRow
19+
label="거래 해시"
20+
value={transaction.hash}
21+
/>
22+
23+
{/* 상태 */}
24+
<DetailRowStatus label="상태" status={transaction.status} />
25+
26+
{/* 블록 */}
27+
<DetailRowBlock
28+
label="블록"
29+
block={transaction.block}
30+
confirmations={transaction.confirmations}
31+
/>
32+
33+
{/* 타임스탬프 */}
34+
<DetailRowIcon
35+
label="타임스탬프"
36+
icon={<Clock className="w-4 h-4 text-gray-400" />}
37+
value={transaction.timestamp}
38+
/>
39+
40+
{/* From / To */}
41+
<DetailRow
42+
label="보낸 사람"
43+
value={transaction.from}
44+
color="orange"
45+
/>
46+
<DetailRow
47+
label="받는 사람"
48+
value={transaction.to}
49+
color="orange"
50+
background
51+
/>
52+
53+
{/* 금액 */}
54+
<SimpleRow label="금액" right={<span className="text-[#FFB020] font-medium">{transaction.value}</span>} />
55+
56+
{/* + 더보기 */}
57+
<div className="px-4 py-4">
58+
<button
59+
onClick={() => setShowMoreDetails(!showMoreDetails)}
60+
className="text-sm font-normal text-[#FFB020]"
61+
>
62+
추가 정보: <span className="underline">{showMoreDetails ? "접기" : "+ 더 보기"}</span>
63+
</button>
64+
65+
{showMoreDetails && (
66+
<div className="mt-4 pt-4 border-t border-gray-200 space-y-3 text-sm">
67+
<DetailInfo label="Gas 사용량" value={transaction.gasUsed.toLocaleString()} />
68+
<DetailInfo label="Gas 한도" value={transaction.gasLimit.toLocaleString()} />
69+
<DetailInfo label="Gas 가격" value={`${transaction.gasPrice} Gwei`} />
70+
<DetailInfo label="Nonce" value={transaction.nonce} />
71+
</div>
72+
)}
73+
</div>
74+
</div>
75+
)
76+
}
77+
78+
// === 내부 컴포넌트 ===
79+
80+
function DetailRow({
81+
label,
82+
value,
83+
background = false,
84+
color = "default",
85+
}: {
86+
label: string
87+
value: string
88+
background?: boolean
89+
color?: "orange" | "default"
90+
}) {
91+
return (
92+
<div className={`flex flex-col px-4 py-4 border-b border-gray-100 ${background ? "bg-gray-50" : ""}`}>
93+
<LabelWithIcon text={label} />
94+
<div className={`mt-1 break-all font-mono text-xs ${color === "orange" ? "text-[#FFB020]" : "text-[#111827]"}`}>
95+
{value}
96+
</div>
97+
</div>
98+
)
99+
}
100+
101+
function DetailRowStatus({ label, status }: { label: string; status: string }) {
102+
return (
103+
<div className="flex items-center justify-between px-4 py-4 border-b border-gray-100 bg-gray-50">
104+
<LabelWithIcon text={label} />
105+
<Badge className="bg-green-100 text-green-700 border-green-200 hover:bg-green-100">
106+
<CheckCircle className="w-3 h-3 mr-1" />
107+
성공
108+
</Badge>
109+
</div>
110+
)
111+
}
112+
113+
function DetailRowBlock({ label, block, confirmations }: {
114+
label: string
115+
block: number
116+
confirmations: number
117+
}) {
118+
return (
119+
<div className="flex items-center justify-between px-4 py-4 border-b border-gray-100">
120+
<LabelWithIcon text={label} />
121+
<div className="flex items-center gap-2">
122+
<CheckCircle className="w-4 h-4 text-green-500" />
123+
<span className="text-[#FFB020] font-medium text-sm">{block}</span>
124+
<span className="text-gray-500 text-xs">{confirmations} 확인</span>
125+
</div>
126+
</div>
127+
)
128+
}
129+
130+
function DetailRowIcon({ label, icon, value }: {
131+
label: string
132+
icon: React.ReactNode
133+
value: string
134+
}) {
135+
return (
136+
<div className="flex items-center justify-between px-4 py-4 border-b border-gray-100 bg-gray-50">
137+
<LabelWithIcon text={label} />
138+
<div className="flex items-center gap-2 text-sm text-[#111827]">
139+
{icon}
140+
<span>{value}</span>
141+
</div>
142+
</div>
143+
)
144+
}
145+
146+
function SimpleRow({ label, right, background = false }: {
147+
label: string
148+
right: React.ReactNode
149+
background?: boolean
150+
}) {
151+
return (
152+
<div className={`flex items-center justify-between px-4 py-4 border-b border-gray-100 ${background ? "bg-gray-50" : ""}`}>
153+
<LabelWithIcon text={label} />
154+
{right}
155+
</div>
156+
)
157+
}
158+
159+
function DetailInfo({ label, value }: { label: string; value: string | number }) {
160+
return (
161+
<div className="flex items-center justify-between">
162+
<span className="text-sm text-[#6B7280]">{label}:</span>
163+
<span className="text-sm font-medium text-[#111827]">{value}</span>
164+
</div>
165+
)
166+
}
167+
168+
function LabelWithIcon({ text }: { text: string }) {
169+
return (
170+
<div className="flex items-center gap-2">
171+
<div className="w-4 h-4 rounded-full border border-gray-400 flex items-center justify-center">
172+
<span className="text-xs text-gray-500">?</span>
173+
</div>
174+
<span className="text-sm text-[#6B7280]">{text}:</span>
175+
</div>
176+
)
177+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface BlockchainTransaction {
2+
id: string
3+
hash: string
4+
status: "success" | "pending" | "failed"
5+
block: number
6+
confirmations: number
7+
timestamp: string
8+
from: string
9+
to: string
10+
value: string
11+
fee: string
12+
gasPrice: string
13+
gasUsed: number
14+
gasLimit: number
15+
nonce: number
16+
}

app/wallet/totaltransaction/[id]/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { motion } from "framer-motion";
66
import Header from "@/components/common/Header";
77
import TransactionCardContent from "@/app/wallet/components/common/TransactionCardContent";
88
import {fetchTransactionDetail} from "@/app/wallet/api/fetch-transactions-detail";
9+
import Link from "next/link";
910

1011

1112
interface TransactionDetail {
@@ -14,6 +15,7 @@ interface TransactionDetail {
1415
amount: number;
1516
displayDescription: string;
1617
createdAt: string;
18+
txHash: string;
1719
}
1820

1921
export default function TransactionDetailPage() {
@@ -123,7 +125,12 @@ export default function TransactionDetailPage() {
123125

124126
<div className="flex justify-between py-4">
125127
<div className="text-gray-500">거래 설명</div>
126-
<div className="font-medium">{transaction.displayDescription}</div>
128+
<Link
129+
href={`/wallet/blockchain-details/${transaction.txHash}`}
130+
className="text-[#FFB020] underline hover:text-[#f29d00] transition-colors duration-150"
131+
>
132+
{transaction.txHash.slice(0, 10)}...{transaction.txHash.slice(-6)} 🔗
133+
</Link>
127134
</div>
128135
</motion.div>
129136

middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const protectedPaths = [
1616
"/merchant/dashboard",
1717
"/merchant/wallet",
1818
"/merchant/notifications",
19-
"merchant/mypage"
19+
"/merchant/mypage"
2020

2121
];
2222

0 commit comments

Comments
 (0)