import { useMemo } from 'react' // 최근 N일: 위 = 활성 고객 수(선), 아래 = 일별 신규(+)/해지(−) 막대. x축(날짜) 공유. // 절대 수와 일별 증감은 스케일이 완전히 달라서 한 영역에 겹치지 않고 위/아래로 분리. const W = 720, H = 320 const X0 = 40, X1 = W - 28 const L_TOP = 44, L_BOT = 118 // 위: 선 영역 const B_TOP = 182, B_BOT = 268 // 아래: 막대 영역 const DATE_Y = H - 10 const ACCENT = '#2e7d32' // 선 / 신규(+) const REM = '#d64545' // 해지(−) const parseYmd = (s) => { const [, m, d] = String(s).split('-').map(Number) return { m, d } } export default function ActiveCustomerTrendLine({ points }) { const model = useMemo(() => { const pts = (points ?? []).map((p) => ({ ...parseYmd(p.date), count: Number(p.count ?? 0), added: Number(p.added ?? 0), removed: Number(p.removed ?? 0), })) if (!pts.length) return { pts: [] } const n = pts.length const xOf = (i) => (n === 1 ? (X0 + X1) / 2 : X0 + ((X1 - X0) * i) / (n - 1)) // 위: 선 스케일 (절대 수 — 변화가 작으니 데이터 범위에 맞춰 확대) const cs = pts.map((p) => p.count) let lo = Math.min(...cs), hi = Math.max(...cs) const pad = lo === hi ? Math.max(1, Math.round(Math.abs(hi) * 0.02)) : (hi - lo) * 0.6 lo -= pad; hi += pad if (lo === hi) hi = lo + 1 const yLine = (v) => L_BOT - ((v - lo) / (hi - lo)) * (L_BOT - L_TOP) const linePath = pts .map((p, i) => `${i === 0 ? 'M' : 'L'} ${xOf(i).toFixed(2)},${yLine(p.count).toFixed(2)}`) .join(' ') // 아래: 막대 스케일 (그 주 증감 최대치 기준 단일 스케일) const maxUp = Math.max(0, ...pts.map((p) => p.added)) const maxDown = Math.max(0, ...pts.map((p) => p.removed)) const denom = maxUp + maxDown const ppu = denom > 0 ? (B_BOT - B_TOP) / denom : 0 const baseline = denom > 0 ? B_TOP + maxUp * ppu : (B_TOP + B_BOT) / 2 const spacing = n > 1 ? (X1 - X0) / (n - 1) : (X1 - X0) const bw = Math.min(26, Math.max(12, spacing * 0.42)) return { pts, xOf, yLine, linePath, lastIdx: n - 1, ppu, baseline, bw, hasBars: denom > 0 } }, [points]) if (!model.pts.length) { return