fix(statistics): 월간/일자별 매출 — 좌측 리스트 / 우측 차트 50:50 레이아웃
Deploy momo-erp / deploy (push) Successful in 2m9s
Deploy momo-erp / deploy (push) Successful in 2m9s
This commit is contained in:
@@ -94,58 +94,66 @@ export default function DailyStatsPage() {
|
||||
<Card label="총 매출 (VAT)" value={`₩${fmt(total)}`} color="emerald" />
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-xl p-4">
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm">일별 매출 추이</h3>
|
||||
<div className="w-full h-72 sm:h-80">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">불러오는 중...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">데이터가 없습니다.</div>
|
||||
) : (
|
||||
<ResponsiveContainer>
|
||||
<ComposedChart data={chartData} margin={{ top: 10, right: 20, bottom: 0, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="day" tick={{ fontSize: 10 }} />
|
||||
<YAxis yAxisId="left" tick={{ fontSize: 10 }} tickFormatter={(v) => `${(v / 10000).toFixed(0)}만`} />
|
||||
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} />
|
||||
<Tooltip
|
||||
formatter={(v, name) => name === "건수" ? `${Number(v)}건` : `₩${fmt(Number(v))}`}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar yAxisId="left" dataKey="면세" stackId="a" fill="#8b5cf6" />
|
||||
<Bar yAxisId="left" dataKey="과세" stackId="a" fill="#f43f5e" />
|
||||
<Line yAxisId="right" type="monotone" dataKey="건수" stroke="#0ea5e9" strokeWidth={2} dot={{ r: 3 }} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
{/* 좌: 일자별 리스트 / 우: 추이 차트 — 50/50 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div className="bg-white border rounded-xl overflow-hidden flex flex-col" style={{ maxHeight: "calc(100vh - 320px)" }}>
|
||||
<div className="px-3 py-2 bg-slate-50 border-b border-slate-200 text-xs font-semibold text-slate-600">
|
||||
일자별 매출 ({rows.length}일)
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[480px]">
|
||||
<thead className="bg-slate-50 text-slate-600 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2">일자</th>
|
||||
<th className="text-right px-3 py-2">건수</th>
|
||||
<th className="text-right px-3 py-2">면세</th>
|
||||
<th className="text-right px-3 py-2">과세</th>
|
||||
<th className="text-right px-3 py-2">합계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={5} className="text-center py-12 text-slate-400">{loading ? "조회 중..." : "선택한 기간의 매출 데이터가 없습니다."}</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.DAY} className="border-t border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-3 py-2 font-semibold">{r.DAY}</td>
|
||||
<td className="px-3 py-2 text-right">{r.ORDER_CNT}건</td>
|
||||
<td className="px-3 py-2 text-right text-violet-700">₩{fmt(r.TAX_FREE)}</td>
|
||||
<td className="px-3 py-2 text-right text-rose-700">₩{fmt(r.TAXABLE)}</td>
|
||||
<td className="px-3 py-2 text-right font-bold">₩{fmt(r.TOTAL)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[600px]">
|
||||
<thead className="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3">일자</th>
|
||||
<th className="text-right px-4 py-3">건수</th>
|
||||
<th className="text-right px-4 py-3">면세</th>
|
||||
<th className="text-right px-4 py-3">과세</th>
|
||||
<th className="text-right px-4 py-3">합계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={5} className="text-center py-12 text-slate-400">선택한 기간의 매출 데이터가 없습니다.</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.DAY} className="border-t border-slate-100">
|
||||
<td className="px-4 py-2.5 font-semibold">{r.DAY}</td>
|
||||
<td className="px-4 py-2.5 text-right">{r.ORDER_CNT}건</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums text-violet-700">₩{fmt(r.TAX_FREE)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums text-rose-700">₩{fmt(r.TAXABLE)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums font-bold">₩{fmt(r.TOTAL)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="bg-white border rounded-xl p-4 flex flex-col" style={{ maxHeight: "calc(100vh - 320px)" }}>
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm shrink-0">일별 매출 추이</h3>
|
||||
<div className="flex-1 min-h-0">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">불러오는 중...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">데이터가 없습니다.</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={chartData} margin={{ top: 10, right: 20, bottom: 0, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="day" tick={{ fontSize: 10 }} />
|
||||
<YAxis yAxisId="left" tick={{ fontSize: 10 }} tickFormatter={(v) => `${(v / 10000).toFixed(0)}만`} />
|
||||
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} />
|
||||
<Tooltip
|
||||
formatter={(v, name) => name === "건수" ? `${Number(v)}건` : `₩${fmt(Number(v))}`}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar yAxisId="left" dataKey="면세" stackId="a" fill="#8b5cf6" />
|
||||
<Bar yAxisId="left" dataKey="과세" stackId="a" fill="#f43f5e" />
|
||||
<Line yAxisId="right" type="monotone" dataKey="건수" stroke="#0ea5e9" strokeWidth={2} dot={{ r: 3 }} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -100,59 +100,66 @@ export default function StatisticsPage() {
|
||||
<Card label="총 매출 (VAT포함)" value={fmt(grandTotal)} color="emerald" />
|
||||
</div>
|
||||
|
||||
{/* 차트 */}
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm">업체별 매출 (TOP 15)</h3>
|
||||
<div className="w-full h-72 sm:h-80">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">불러오는 중...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">데이터가 없습니다.</div>
|
||||
) : (
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={chartData} margin={{ top: 10, right: 10, bottom: 30, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="name" tick={{ fontSize: 11 }} interval={0} angle={-25} textAnchor="end" height={50} />
|
||||
<YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => `${(v / 10000).toFixed(0)}만`} />
|
||||
<Tooltip
|
||||
formatter={(v) => `₩${fmt(Number(v))}`}
|
||||
labelFormatter={(_, payload) => (payload?.[0]?.payload as { fullName: string })?.fullName ?? ""}
|
||||
cursor={{ fill: "rgba(16, 185, 129, 0.05)" }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="면세" stackId="a" fill="#8b5cf6" />
|
||||
<Bar dataKey="과세" stackId="a" fill="#f43f5e">
|
||||
{chartData.map((_, i) => <Cell key={i} fill={COLORS[i % COLORS.length]} fillOpacity={0.7} />)}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
{/* 좌: 업체별 리스트 / 우: 차트 — 50/50 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden flex flex-col" style={{ maxHeight: "calc(100vh - 320px)" }}>
|
||||
<div className="px-3 py-2 bg-slate-50 border-b border-slate-200 text-xs font-semibold text-slate-600">
|
||||
업체별 매출 ({rows.length}건)
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[480px]">
|
||||
<thead className="bg-slate-50 text-slate-600 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2">업체명</th>
|
||||
<th className="text-right px-3 py-2">면세</th>
|
||||
<th className="text-right px-3 py-2">과세</th>
|
||||
<th className="text-right px-3 py-2">총 매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={4} className="text-center py-12 text-slate-400">{loading ? "조회 중..." : "선택한 월의 매출 데이터가 없습니다."}</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.COMPANY_NAME} className="border-t border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-3 py-2 font-semibold">{r.COMPANY_NAME}</td>
|
||||
<td className="px-3 py-2 text-right text-violet-700">{fmt(r.TAX_FREE)}</td>
|
||||
<td className="px-3 py-2 text-right text-rose-700">{fmt(r.TAXABLE)}</td>
|
||||
<td className="px-3 py-2 text-right font-bold">₩{fmt(r.TOTAL)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[600px]">
|
||||
<thead className="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3">업체명</th>
|
||||
<th className="text-right px-4 py-3">면세 합계</th>
|
||||
<th className="text-right px-4 py-3">과세 공급가</th>
|
||||
<th className="text-right px-4 py-3">총 매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={4} className="text-center py-12 text-slate-400">선택한 월의 매출 데이터가 없습니다.</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.COMPANY_NAME} className="border-t border-slate-100">
|
||||
<td className="px-4 py-3 font-semibold">{r.COMPANY_NAME}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums text-violet-700">{fmt(r.TAX_FREE)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums text-rose-700">{fmt(r.TAXABLE)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-bold">₩{fmt(r.TOTAL)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 flex flex-col" style={{ maxHeight: "calc(100vh - 320px)" }}>
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm shrink-0">업체별 매출 (TOP 15)</h3>
|
||||
<div className="flex-1 min-h-0">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">불러오는 중...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">데이터가 없습니다.</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 10, right: 10, bottom: 30, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="name" tick={{ fontSize: 11 }} interval={0} angle={-25} textAnchor="end" height={50} />
|
||||
<YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => `${(v / 10000).toFixed(0)}만`} />
|
||||
<Tooltip
|
||||
formatter={(v) => `₩${fmt(Number(v))}`}
|
||||
labelFormatter={(_, payload) => (payload?.[0]?.payload as { fullName: string })?.fullName ?? ""}
|
||||
cursor={{ fill: "rgba(16, 185, 129, 0.05)" }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="면세" stackId="a" fill="#8b5cf6" />
|
||||
<Bar dataKey="과세" stackId="a" fill="#f43f5e">
|
||||
{chartData.map((_, i) => <Cell key={i} fill={COLORS[i % COLORS.length]} fillOpacity={0.7} />)}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user