This commit is contained in:
@@ -98,62 +98,70 @@ export default function MarginPage() {
|
||||
<Card label="마진율" value={`${marginPct}%`} color="violet" />
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-xl p-4">
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm">마진 TOP 10 품목</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: 20, bottom: 30, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="name" tick={{ fontSize: 10 }} 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 ?? ""}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="원가" fill="#f59e0b" />
|
||||
<Bar dataKey="마진" fill="#10b981" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[700px]">
|
||||
<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>
|
||||
<th className="text-right px-4 py-3">마진율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={6} className="text-center py-12 text-slate-400">데이터가 없습니다.</td></tr>
|
||||
) : rows.map((r) => {
|
||||
const pct = Number(r.REVENUE) ? ((Number(r.MARGIN) / Number(r.REVENUE)) * 100).toFixed(1) : "0.0";
|
||||
return (
|
||||
<tr key={r.ITEM_CODE} className="border-t border-slate-100">
|
||||
<td className="px-4 py-2.5 font-semibold">{r.ITEM_NAME}</td>
|
||||
<td className="px-4 py-2.5 text-right">{fmt(r.QTY)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums">₩{fmt(r.REVENUE)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums text-amber-700">₩{fmt(r.COST)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums font-bold text-emerald-700">₩{fmt(r.MARGIN)}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums">{pct}%</td>
|
||||
{/* 좌: 품목 리스트 / 우: 차트 — 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 - 280px)" }}>
|
||||
<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-[560px]">
|
||||
<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>
|
||||
<th className="text-right px-3 py-2">%</th>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={6} className="text-center py-12 text-slate-400">{loading ? "조회 중..." : "데이터가 없습니다."}</td></tr>
|
||||
) : rows.map((r) => {
|
||||
const pct = Number(r.REVENUE) ? ((Number(r.MARGIN) / Number(r.REVENUE)) * 100).toFixed(1) : "0.0";
|
||||
return (
|
||||
<tr key={r.ITEM_CODE} className="border-t border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-3 py-2 font-semibold">{r.ITEM_NAME}</td>
|
||||
<td className="px-3 py-2 text-right">{fmt(r.QTY)}</td>
|
||||
<td className="px-3 py-2 text-right">₩{fmt(r.REVENUE)}</td>
|
||||
<td className="px-3 py-2 text-right text-amber-700">₩{fmt(r.COST)}</td>
|
||||
<td className="px-3 py-2 text-right font-bold text-emerald-700">₩{fmt(r.MARGIN)}</td>
|
||||
<td className="px-3 py-2 text-right">{pct}%</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-xl p-4 flex flex-col" style={{ maxHeight: "calc(100vh - 280px)" }}>
|
||||
<h3 className="font-bold text-slate-700 mb-3 text-sm shrink-0">마진 TOP 10 품목</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: 20, bottom: 30, left: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="name" tick={{ fontSize: 10 }} 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 ?? ""}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="원가" fill="#f59e0b" />
|
||||
<Bar dataKey="마진" fill="#10b981" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user