PaperAccountCard.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. 'use client';
  2. import { useCallback, useEffect, useState } from 'react';
  3. import { fetchApi, cn } from '@/lib/utils/client';
  4. import { formatBpPercent, bpDirection, formatTokenNumber } from '@/lib/utils/paper';
  5. import type {
  6. PaperAccountResponse,
  7. PaperMyRankResponse
  8. } from '@/types/paper';
  9. import Loading from '@/app/component/Loading';
  10. import PaperDepositWithdrawModal from './PaperDepositWithdrawModal';
  11. // 내 계좌 카드 — 잔액/예약/평가자산/좌수/누적수익률/순위 + 입금·출금 모달.
  12. // 계좌 미개설(hasAccount=false) 시 첫 입금 안내.
  13. export default function PaperAccountCard()
  14. {
  15. const [account, setAccount] = useState<PaperAccountResponse|null>(null);
  16. const [rank, setRank] = useState<PaperMyRankResponse|null>(null);
  17. const [loading, setLoading] = useState(true);
  18. const [modal, setModal] = useState<'deposit'|'withdraw'|null>(null);
  19. const load = useCallback(async () => {
  20. setLoading(true);
  21. const [accRes, rankRes] = await Promise.all([
  22. fetchApi<PaperAccountResponse>('/api/paper/account', { silent: true }),
  23. fetchApi<PaperMyRankResponse>('/api/paper/leaderboard/me?sort=return&period=all', { silent: true })
  24. ]);
  25. setAccount(accRes.success ? (accRes.data ?? null) : null);
  26. setRank(rankRes.success ? (rankRes.data ?? null) : null);
  27. setLoading(false);
  28. }, []);
  29. useEffect(() => {
  30. load();
  31. }, [load]);
  32. const handleDone = useCallback(() => {
  33. setModal(null);
  34. load();
  35. }, [load]);
  36. if (loading) {
  37. return (
  38. <div className='paper__account-card'>
  39. <Loading type={2} />
  40. </div>
  41. );
  42. }
  43. if (!account || !account.hasAccount) {
  44. return (
  45. <>
  46. <div className='paper__empty-account'>
  47. <div className='paper__empty-account-title'>아직 모의투자 계좌가 없어요</div>
  48. <p className='paper__empty-account-desc'>
  49. 지갑 토큰을 입금하면 계좌가 자동으로 개설됩니다.<br />
  50. 입금한 토큰으로 실제 종목을 매매하고, 언제든 다시 지갑으로 출금할 수 있어요.
  51. </p>
  52. <button type='button' className='paper__btn paper__btn--primary' onClick={() => setModal('deposit')}>
  53. 첫 입금하고 시작하기
  54. </button>
  55. </div>
  56. {modal === 'deposit' && (
  57. <PaperDepositWithdrawModal
  58. mode='deposit'
  59. account={null}
  60. onClose={() => setModal(null)}
  61. onDone={handleDone}
  62. />
  63. )}
  64. </>
  65. );
  66. }
  67. const returnDir = bpDirection(account.cumReturnBp);
  68. return (
  69. <>
  70. <div className='paper__account-card'>
  71. <div className='paper__metrics'>
  72. <div className='paper__metric'>
  73. <span className='paper__metric-label'>가용 토큰</span>
  74. <span className='paper__metric-value'>{formatTokenNumber(account.token)}</span>
  75. </div>
  76. <div className='paper__metric'>
  77. <span className='paper__metric-label'>매수 예약</span>
  78. <span className='paper__metric-value'>{formatTokenNumber(account.reservedToken)}</span>
  79. </div>
  80. <div className='paper__metric'>
  81. <span className='paper__metric-label'>평가자산</span>
  82. <span className='paper__metric-value'>{formatTokenNumber(account.equity)}</span>
  83. </div>
  84. <div className='paper__metric'>
  85. <span className='paper__metric-label'>보유 좌수</span>
  86. <span className='paper__metric-value'>{account.units.toLocaleString('ko-KR', { maximumFractionDigits: 4 })}</span>
  87. </div>
  88. <div className='paper__metric'>
  89. <span className='paper__metric-label'>누적 수익률</span>
  90. <span className={cn('paper__metric-value', `paper__value--${returnDir}`)}>{formatBpPercent(account.cumReturnBp)}</span>
  91. </div>
  92. <div className='paper__metric'>
  93. <span className='paper__metric-label'>순위 (수익률·전체)</span>
  94. <span className='paper__metric-value'>
  95. {rank && rank.hasRank ? `${rank.rank.toLocaleString()}위 / ${rank.total.toLocaleString()}` : '미등재'}
  96. </span>
  97. </div>
  98. </div>
  99. <div className='paper__actions'>
  100. <button type='button' className='paper__btn paper__btn--primary' onClick={() => setModal('deposit')}>
  101. 입금
  102. </button>
  103. <button type='button' className='paper__btn' onClick={() => setModal('withdraw')}>
  104. 출금
  105. </button>
  106. </div>
  107. </div>
  108. {modal && (
  109. <PaperDepositWithdrawModal
  110. mode={modal}
  111. account={account}
  112. onClose={() => setModal(null)}
  113. onDone={handleDone}
  114. />
  115. )}
  116. </>
  117. );
  118. }