page.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. 'use client';
  2. import './style.scss';
  3. import { useState, useEffect } from 'react';
  4. import { LoginLogType, WalletLogCategory } from '@/constants/common';
  5. import { fetchApi, getDateTime } from '@/lib/utils/client';
  6. import type { ChargeLogsResponse } from '@/types/response/account/chargeLogs';
  7. import Loading from '@/app/component/Loading';
  8. import Pagination from '@/app/component/Pagination';
  9. import NavTabs from '../navTabs';
  10. const STATUS_MAP: Record<string, { label: string; cls: string }> = {
  11. Paid: { label: '완료', cls: 'status--paid' },
  12. Pending: { label: '대기', cls: 'status--pending' },
  13. WaitingDeposit: { label: '입금대기', cls: 'status--pending' },
  14. Failed: { label: '실패', cls: 'status--failed' },
  15. Cancelled: { label: '취소', cls: 'status--cancelled' }
  16. };
  17. const METHOD_MAP: Record<string, string> = {
  18. Card: '신용카드',
  19. VirtualAccount: '가상계좌',
  20. Mobile: '휴대폰',
  21. Transfer: '계좌이체',
  22. NaverPay: '네이버페이',
  23. KakaoPay: '카카오페이',
  24. Payco: '페이코',
  25. Integrated: '통합결제'
  26. };
  27. const TX_TYPE_MAP: Record<string, string> = {
  28. Charge: '충전',
  29. RewardEarned: '보상 적립',
  30. Spend: '사용',
  31. Refund: '환불',
  32. Lock: '잠금',
  33. Unlock: '잠금 해제',
  34. Adjusted: '운영자 조정',
  35. OrderPay: '상점 결제',
  36. OrderRefund: '주문 환불',
  37. OrderChannelReward: '상점 채널 보상',
  38. OrderRefundChannelReward: '상점 보상 차감'
  39. };
  40. const BALANCE_TYPE_MAP: Record<string, string> = {
  41. PgCharged: 'PG 충전',
  42. Deposit: '예치금',
  43. Reward: '보상',
  44. Airdrop: '에어드랍',
  45. Locked: '잠금',
  46. Adjusted: '운영자 조정',
  47. StoreRevenue: '상점 수익'
  48. };
  49. // 적립류 (+), 사용류 (-) 분류 — UI 표시용 부호
  50. const EARN_TX_TYPES = new Set(['Charge', 'RewardEarned', 'Unlock', 'OrderChannelReward', 'OrderRefund', 'Refund']);
  51. export default function ChargeLogs()
  52. {
  53. const [error, setError] = useState<string>('');
  54. const [loading, setLoading] = useState<boolean>(true);
  55. const [page, setPage] = useState<number>(1);
  56. const [type, setType] = useState<LoginLogType>(LoginLogType.Today);
  57. const [category, setCategory] = useState<WalletLogCategory>(WalletLogCategory.Charge);
  58. const [data, setData] = useState<ChargeLogsResponse>({
  59. total: 0,
  60. list: []
  61. });
  62. useEffect(() => {
  63. if (error) {
  64. alert(error);
  65. setError('');
  66. }
  67. }, [error]);
  68. useEffect(() => {
  69. setLoading(true);
  70. fetchApi<ChargeLogsResponse>(`/api/mypage/charge-logs?type=${type}&category=${category}&page=${page}&perPage=20`).then((res) => {
  71. setData(res.data!);
  72. }).catch(err => {
  73. setError(err.message);
  74. }).finally(() => {
  75. setLoading(false);
  76. });
  77. }, [type, category, page]);
  78. useEffect(() => {
  79. setPage(1);
  80. }, [type, category]);
  81. const tabItems = [
  82. { label: "오늘", value: LoginLogType.Today },
  83. { label: "1주일", value: LoginLogType.Week },
  84. { label: "1개월", value: LoginLogType.Month },
  85. { label: "3개월", value: LoginLogType.QuarterYear },
  86. { label: "6개월", value: LoginLogType.HalfYear }
  87. ];
  88. const categoryItems = [
  89. { label: '전체', value: WalletLogCategory.All },
  90. { label: '충전', value: WalletLogCategory.Charge },
  91. { label: '적립', value: WalletLogCategory.Earn },
  92. { label: '사용', value: WalletLogCategory.Spend },
  93. { label: '환불', value: WalletLogCategory.Refund }
  94. ];
  95. const getStatus = (status: string|null) => status ? (STATUS_MAP[status] ?? { label: status, cls: '' }) : { label: '', cls: '' };
  96. const getMethod = (method: string|null) => method ? (METHOD_MAP[method] ?? method) : '';
  97. const getTxType = (txType: string|null) => txType ? (TX_TYPE_MAP[txType] ?? txType) : '';
  98. const getBalanceType = (bt: string|null) => bt ? (BALANCE_TYPE_MAP[bt] ?? bt) : '';
  99. const isEarn = (txType: string|null) => txType !== null && EARN_TX_TYPES.has(txType);
  100. const isCharge = category === WalletLogCategory.Charge;
  101. return (
  102. <>
  103. <NavTabs />
  104. <div id="chargeLogs">
  105. {loading && <Loading />}
  106. <h1>(P) 충전 내역</h1>
  107. <div className="charge-logs__header">
  108. <div className="charge-logs__summary">합계: {data.total}</div>
  109. <div className="charge-logs__tabs">
  110. <select
  111. className="charge-logs__filter"
  112. value={category}
  113. onChange={(e) => setCategory(e.target.value as WalletLogCategory)}
  114. >
  115. {categoryItems.map((item) => (
  116. <option key={item.value} value={item.value}>{item.label}</option>
  117. ))}
  118. </select>
  119. {tabItems.map((item, i) => (
  120. <button type="button" key={i} className={type === item.value ? 'active' : ''}
  121. onClick={() => setType(item.value)}>{item.label}
  122. </button>
  123. ))}
  124. </div>
  125. </div>
  126. <section className={`charge-logs__list ${isCharge ? 'charge-logs__list--charge' : 'charge-logs__list--wallet'}`}>
  127. <article>
  128. {isCharge ? (
  129. <ul>
  130. <li>일시</li>
  131. <li>주문번호</li>
  132. <li>결제 수단</li>
  133. <li>결제 금액</li>
  134. <li>캐시</li>
  135. <li>상태</li>
  136. </ul>
  137. ) : (
  138. <ul>
  139. <li>일시</li>
  140. <li>구분</li>
  141. <li>사유</li>
  142. <li>잔액 유형</li>
  143. <li>금액</li>
  144. <li>잔액 후</li>
  145. </ul>
  146. )}
  147. </article>
  148. <article>
  149. {data.list.length > 0 ? (
  150. data.list.map((row) => {
  151. if (isCharge || row.category === 'charge') {
  152. const st = getStatus(row.status);
  153. return (
  154. <section key={row.id}>
  155. {/* PC */}
  156. <ol>
  157. <li>{getDateTime(row.paidAt ?? row.createdAt)}</li>
  158. <li className="charge-logs__order-id">{row.orderID}</li>
  159. <li>{getMethod(row.paymentMethod)}</li>
  160. <li>{row.amount.toLocaleString()}원</li>
  161. <li className="amount-plus">+{(row.pointAmount ?? 0).toLocaleString()}P</li>
  162. <li><span className={st.cls}>{st.label}</span></li>
  163. </ol>
  164. {/* Mobile */}
  165. <dl hidden>
  166. <dt>
  167. <div className='flex justify-between'>
  168. <div>{row.amount.toLocaleString()}원 충전</div>
  169. <div>
  170. <small className="charge-logs__order-id">{row.orderID}</small>
  171. </div>
  172. </div>
  173. </dt>
  174. <dd>
  175. <ul>
  176. <li className="amount-plus">+{(row.pointAmount ?? 0).toLocaleString()}P</li>
  177. <li><span className={st.cls}>{st.label}</span></li>
  178. <li>{getDateTime(row.paidAt ?? row.createdAt)}</li>
  179. </ul>
  180. </dd>
  181. </dl>
  182. </section>
  183. );
  184. }
  185. // all/earn/spend/refund 행
  186. const earn = isEarn(row.txType);
  187. const sign = earn ? '+' : '-';
  188. const amountCls = earn ? 'amount-plus' : 'amount-minus';
  189. return (
  190. <section key={row.id}>
  191. {/* PC */}
  192. <ol>
  193. <li>{getDateTime(row.createdAt)}</li>
  194. <li>{getTxType(row.txType)}</li>
  195. <li className="charge-logs__reason" title={row.reason ?? ''}>{row.reason}</li>
  196. <li>{getBalanceType(row.balanceType)}</li>
  197. <li className={amountCls}>{sign}{row.amount.toLocaleString()}P</li>
  198. <li>{(row.balanceAfter ?? 0).toLocaleString()}P</li>
  199. </ol>
  200. {/* Mobile */}
  201. <dl hidden>
  202. <dt>
  203. <div className='flex justify-between'>
  204. <div>{getTxType(row.txType)}</div>
  205. <div className={amountCls}>{sign}{row.amount.toLocaleString()}P</div>
  206. </div>
  207. </dt>
  208. <dd>
  209. <ul>
  210. <li>{getBalanceType(row.balanceType)}</li>
  211. <li className="charge-logs__reason">{row.reason}</li>
  212. <li>{getDateTime(row.createdAt)}</li>
  213. </ul>
  214. </dd>
  215. </dl>
  216. </section>
  217. );
  218. })
  219. ) : (
  220. <p className="empty">기록이 없습니다.</p>
  221. )}
  222. </article>
  223. </section>
  224. {data.list.length > 0 && (
  225. <Pagination total={data.total} page={page} perPage={20} onChange={setPage} />
  226. )}
  227. </div>
  228. </>
  229. );
  230. }