page.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. 'use client';
  2. import { useMemo, useState, useEffect } from 'react';
  3. import Link from 'next/link';
  4. import { LogOut } from 'lucide-react';
  5. import { fetchApi, getDateTime } from '@/lib/utils/client';
  6. import type { WalletBalanceResponse, TransactionCategory } from '@/types/response/wallet/balance';
  7. import Loading from '@/app/component/Loading';
  8. type FilterKey = 'all'|TransactionCategory;
  9. const FILTER_TABS: { key: FilterKey; label: string }[] = [
  10. { key: 'all', label: '전체' },
  11. { key: 'donation', label: '후원 수익' },
  12. { key: 'commission', label: '판매 수수료' }
  13. ];
  14. const CATEGORY_LABEL: Record<TransactionCategory, string> = {
  15. donation: '후원',
  16. commission: '판매 수수료',
  17. other: '기타'
  18. };
  19. const TX_TYPE_LABEL: Record<string, string> = {
  20. DonationIn: '후원 받음',
  21. DonationOut: '후원 보냄',
  22. OrderChannelReward: '상점 판매 보상',
  23. OrderRefundChannelReward: '상점 환불 차감',
  24. WithdrawalStoreRevenue: '판매 수수료 출금',
  25. Charge: '충전',
  26. RewardEarned: '보상 적립',
  27. Spend: '사용',
  28. Refund: '환불',
  29. Lock: '잠금',
  30. Unlock: '잠금 해제',
  31. Adjusted: '운영자 조정'
  32. };
  33. export default function WalletBalancePage() {
  34. const [loading, setLoading] = useState(true);
  35. const [data, setData] = useState<WalletBalanceResponse|null>(null);
  36. const [filter, setFilter] = useState<FilterKey>('all');
  37. useEffect(() => {
  38. fetchApi<WalletBalanceResponse>('/api/studio/wallet/balance')
  39. .then(res => {
  40. if (res.data) {
  41. setData(res.data);
  42. }
  43. })
  44. .catch(() => {})
  45. .finally(() => setLoading(false));
  46. }, []);
  47. const filteredTx = useMemo(() => {
  48. if (!data) {
  49. return [];
  50. }
  51. if (filter === 'all') {
  52. return data.recentTransactions;
  53. }
  54. return data.recentTransactions.filter(t => t.category === filter);
  55. }, [data, filter]);
  56. if (loading || !data) {
  57. return <Loading />;
  58. }
  59. return (
  60. <div className="studio-page wallet">
  61. <div className="studio-page__header">
  62. <h1 className="studio-page__title">잔액 현황</h1>
  63. </div>
  64. {/* Summary Cards */}
  65. <div className="wallet__cards">
  66. <div className="wallet__card">
  67. <span className="wallet__card-label">출금 가능 잔액 (M)</span>
  68. <div className="wallet__card-value wallet__card-value--money">
  69. {data.withdrawableBalance.toLocaleString()}원
  70. </div>
  71. </div>
  72. <div className="wallet__card">
  73. <span className="wallet__card-label">누적 수익</span>
  74. <div className="wallet__card-value">
  75. {data.totalEarned.toLocaleString()}원
  76. </div>
  77. </div>
  78. <div className="wallet__card">
  79. <span className="wallet__card-label">누적 출금</span>
  80. <div className="wallet__card-value">
  81. {data.totalWithdrawn.toLocaleString()}원
  82. </div>
  83. </div>
  84. <div className="wallet__card">
  85. <span className="wallet__card-label">누적 판매 수수료</span>
  86. <div className="wallet__card-value">
  87. {data.totalStoreCommission.toLocaleString()}원
  88. </div>
  89. </div>
  90. </div>
  91. {/* Actions */}
  92. <div className="wallet__actions">
  93. <Link href="/studio/wallet/withdraw" className="wallet__action-btn wallet__action-btn--primary">
  94. <LogOut className="size-4" />
  95. 출금하기
  96. </Link>
  97. </div>
  98. {/* Recent Transactions */}
  99. <h2 className="wallet__section-title">최근 거래 내역</h2>
  100. <div className="wallet__filter flex gap-2 mb-3 flex-wrap">
  101. {FILTER_TABS.map(t => (
  102. <button
  103. key={t.key}
  104. type="button"
  105. onClick={() => setFilter(t.key)}
  106. className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors ${
  107. filter === t.key
  108. ? 'bg-blue-600 border-blue-600 text-white dark:bg-blue-500 dark:border-blue-500'
  109. : 'bg-transparent border-neutral-300 text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800'
  110. }`}
  111. >
  112. {t.label}
  113. </button>
  114. ))}
  115. </div>
  116. <div className="wallet__table-wrap">
  117. <table className="wallet__table">
  118. <thead>
  119. <tr>
  120. <th>일시</th>
  121. <th>카테고리</th>
  122. <th>유형</th>
  123. <th>내용</th>
  124. <th className="text-right">금액</th>
  125. <th className="text-right">잔액</th>
  126. </tr>
  127. </thead>
  128. <tbody>
  129. {filteredTx.length > 0 ? (
  130. filteredTx.map((tx) => (
  131. <tr key={tx.id}>
  132. <td>{getDateTime(tx.createdAt)}</td>
  133. <td>{CATEGORY_LABEL[tx.category]}</td>
  134. <td>{TX_TYPE_LABEL[tx.type] ?? tx.type}</td>
  135. <td>
  136. {tx.refID && (
  137. <span className="font-mono text-xs text-neutral-500 mr-2">{tx.refID}</span>
  138. )}
  139. {tx.description}
  140. </td>
  141. <td className="text-right">
  142. <span className={tx.amount >= 0 ? 'wallet__amount--plus' : 'wallet__amount--minus'}>
  143. {tx.amount >= 0 ? '+' : ''}{tx.amount.toLocaleString()}원
  144. </span>
  145. </td>
  146. <td className="text-right">{tx.balance.toLocaleString()}원</td>
  147. </tr>
  148. ))
  149. ) : (
  150. <tr>
  151. <td colSpan={6} className="wallet__empty">거래 내역이 없습니다.</td>
  152. </tr>
  153. )}
  154. </tbody>
  155. </table>
  156. </div>
  157. </div>
  158. );
  159. }