| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { LoginLogType } from '@/constants/common';
- import { fetchApi, getDateTime } from '@/lib/utils/client';
- import type { WalletRevenueResponse, RevenueChartItem } from '@/types/response/wallet/revenue';
- import { PERIOD_TABS, REVENUE_TYPE_MAP } from '../constants';
- import Loading from '@/app/component/Loading';
- import Pagination from '@/app/component/Pagination';
- import {
- ResponsiveContainer,
- AreaChart,
- Area,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- } from 'recharts';
- function RevenueChart({ chartData }: { chartData: RevenueChartItem[] }) {
- if (chartData.length === 0) {
- return <div className="wallet__chart-empty">데이터가 없습니다.</div>;
- }
- return (
- <ResponsiveContainer width="100%" height={280}>
- <AreaChart data={chartData} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
- <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
- <XAxis
- dataKey="date"
- tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
- axisLine={{ stroke: 'hsl(var(--border))' }}
- tickLine={false}
- />
- <YAxis
- tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
- axisLine={false}
- tickLine={false}
- tickFormatter={(v: number) => v >= 10000 ? `${(v / 10000).toFixed(0)}만` : v.toLocaleString()}
- width={50}
- />
- <Tooltip
- formatter={(value, name) => [
- `${Number(value).toLocaleString()}원`,
- name === 'netAmount' ? '순수익' : '수수료',
- ]}
- labelFormatter={(label) => String(label)}
- contentStyle={{
- background: 'hsl(var(--background))',
- border: '1px solid hsl(var(--border))',
- borderRadius: '6px',
- fontSize: '0.8125rem',
- }}
- />
- <Area
- type="monotone"
- dataKey="netAmount"
- name="netAmount"
- stroke="hsl(var(--primary))"
- fill="hsl(var(--primary))"
- fillOpacity={0.15}
- strokeWidth={2}
- />
- <Area
- type="monotone"
- dataKey="platformFee"
- name="platformFee"
- stroke="hsl(var(--muted-foreground))"
- fill="hsl(var(--muted-foreground))"
- fillOpacity={0.08}
- strokeWidth={1.5}
- />
- </AreaChart>
- </ResponsiveContainer>
- );
- }
- export default function WalletRevenuePage() {
- const [loading, setLoading] = useState(true);
- const [page, setPage] = useState(1);
- const [period, setPeriod] = useState<LoginLogType>(LoginLogType.Month);
- const [data, setData] = useState<WalletRevenueResponse>({ total: 0, summary: { grossAmount: 0, platformFee: 0, netAmount: 0 }, list: [] });
- const [chartData, setChartData] = useState<RevenueChartItem[]>([]);
- useEffect(() => {
- setLoading(true);
- fetchApi<WalletRevenueResponse & { chartData?: RevenueChartItem[] }>(`/api/studio/wallet/revenue?period=${period}&page=${page}&perPage=20`)
- .then(res => {
- if (res.data) {
- setData(res.data);
- setChartData(res.data.chartData ?? []);
- }
- })
- .catch(() => {})
- .finally(() => setLoading(false));
- }, [period, page]);
- useEffect(() => {
- setPage(1);
- }, [period]);
- return (
- <div className="studio-page wallet">
- <div className="studio-page__header">
- <h1 className="studio-page__title">수익 내역</h1>
- </div>
- {loading && <Loading />}
- {/* Summary Cards */}
- <div className="wallet__cards">
- <div className="wallet__card">
- <span className="wallet__card-label">총 후원 금액</span>
- <div className="wallet__card-value">
- {data.summary.grossAmount.toLocaleString()}원
- </div>
- </div>
- <div className="wallet__card">
- <span className="wallet__card-label">플랫폼 수수료</span>
- <div className="wallet__card-value wallet__card-value--danger">
- -{data.summary.platformFee.toLocaleString()}원
- </div>
- </div>
- <div className="wallet__card">
- <span className="wallet__card-label">순수익</span>
- <div className="wallet__card-value wallet__card-value--money">
- {data.summary.netAmount.toLocaleString()}원
- </div>
- </div>
- </div>
- {/* Period Filter */}
- <div className="wallet__header">
- <div />
- <div className="wallet__tabs">
- {PERIOD_TABS.map((tab) => (
- <button
- type="button"
- key={tab.value}
- className={period === tab.value ? 'active' : ''}
- onClick={() => setPeriod(tab.value)}
- >
- {tab.label}
- </button>
- ))}
- </div>
- </div>
- {/* Chart */}
- <div className="wallet__chart-wrap">
- <RevenueChart chartData={chartData} />
- <div style={{ display: 'flex', justifyContent: 'center', gap: '20px', marginTop: '8px', fontSize: '0.8125rem' }}>
- <span><span style={{ color: 'hsl(var(--primary))' }}>■</span> 순수익</span>
- <span><span style={{ color: 'hsl(var(--muted-foreground))' }}>■</span> 수수료</span>
- </div>
- </div>
- {/* Table Header */}
- <div className="wallet__header">
- <div className="wallet__summary">합계: {data.total}건</div>
- </div>
- {/* Table */}
- <div className="wallet__table-wrap">
- <table className="wallet__table">
- <thead>
- <tr>
- <th>일시</th>
- <th>후원자</th>
- <th>유형</th>
- <th style={{ textAlign: 'right' }}>후원 금액</th>
- <th style={{ textAlign: 'right' }}>수수료</th>
- <th style={{ textAlign: 'right' }}>순수익</th>
- </tr>
- </thead>
- <tbody>
- {data.list.length > 0 ? (
- data.list.map((row) => (
- <tr key={row.id}>
- <td>{getDateTime(row.createdAt)}</td>
- <td>{row.donorName}</td>
- <td>
- {REVENUE_TYPE_MAP[row.type] ?? row.type}
- {row.crewName && <small style={{ marginLeft: '4px', color: 'hsl(var(--muted-foreground))' }}>({row.crewName})</small>}
- </td>
- <td style={{ textAlign: 'right' }}>{row.grossAmount.toLocaleString()}원</td>
- <td style={{ textAlign: 'right' }}>
- <span className="wallet__amount--minus">-{row.platformFee.toLocaleString()}원</span>
- </td>
- <td style={{ textAlign: 'right' }}>
- <span className="wallet__amount--plus">{row.netAmount.toLocaleString()}원</span>
- </td>
- </tr>
- ))
- ) : (
- <tr>
- <td colSpan={6} className="wallet__empty">수익 내역이 없습니다.</td>
- </tr>
- )}
- </tbody>
- </table>
- </div>
- {data.total > 0 && (
- <Pagination total={data.total} page={page} perPage={20} onChange={setPage} />
- )}
- </div>
- );
- }
|