o2o-negosium-original/negodata/front/src/features/statistics/components/SavingsTrendChart.tsx
Mina Choi c32ee300e6 [feat] negodata: 통계 페이지(성과 분석) 추가 — 파생 집계 API + recharts 차트
- 백엔드 GET /v1/statistics/summary: 회사/내견적 스코프, 최근 6개월 파생 집계(저장 X, DDL 0)
- 지표: 총절감/절감률/낙찰률/앵커도달률/마감수/재견적라운드 + 월별추이·마감결과분해·참여율·유형별(협상/견적)·카테고리·카드빈도
- 프론트 recharts+shadcn Chart, 사이드바 통계 nav, /statistics

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:18:33 +09:00

42 lines
2.0 KiB
TypeScript

import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { ChartContainer, ChartTooltip, type ChartConfig } from '@/components/ui/chart';
import { Typography } from '@/components/ui/typography';
import { themeOf } from '../palette';
import { won, wonCompact, pct, monthLabel } from '../fmt';
import type { MonthPoint } from '../types';
// 월별 목표가 대비 절감액(단일 시리즈=크기). 한 축 원칙 — 절감률은 이중축 대신 툴팁에 얹는다.
const config = {
savings: { label: '절감액', theme: themeOf('emerald') },
} satisfies ChartConfig;
export function SavingsTrendChart({ data }: { data: MonthPoint[] }) {
return (
<ChartContainer config={config} className="aspect-auto h-56 w-full">
<BarChart data={data} margin={{ top: 8, right: 8, left: 4, bottom: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={monthLabel} />
<YAxis tickLine={false} axisLine={false} width={44} tickFormatter={(v) => wonCompact(Number(v))} />
<ChartTooltip cursor={false} content={<TrendTooltip />} />
<Bar dataKey="savings" fill="var(--color-savings)" radius={[4, 4, 0, 0]} maxBarSize={48} />
</BarChart>
</ChartContainer>
);
}
// 절감액(₩) + 절감률을 한 툴팁에. payload[0].payload = MonthPoint.
function TrendTooltip({ active, payload }: { active?: boolean; payload?: { payload: MonthPoint }[] }) {
if (!active || !payload?.length) return null;
const p = payload[0].payload;
return (
<div className="rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl">
<Typography as="p" variant="caption" className="mb-0.5 font-medium text-foreground">
{monthLabel(p.month)}
</Typography>
<Typography as="p" variant="caption" className="font-mono text-foreground">
{won(p.savings)} · {pct(p.rate)}
</Typography>
</div>
);
}