/** * pi-stats — 출력 포매팅 (테이블 / 일자별 / JSON) */ import { sumBuckets, TOOLS_KEY, SUMMARY_KEY } from "./aggregate.ts"; import type { AggregateResult } from "./aggregate.ts"; const fmtInt = (n: number): string => Math.round(n).toLocaleString("en-US"); const fmtCost = (n: number): string => `$${n.toFixed(4)}`; function column(rows: string[][]): string { // rows[r][c] — 마지막 열 제외 오른쪽 2칸 패딩, 각 열은 셀 최대 폭에 맞춤 const widths: number[] = []; for (const row of rows) { row.forEach((cell, i) => { widths[i] = Math.max(widths[i] ?? 0, cell.length); }); } return rows .map((row) => row .map((cell, i) => i === row.length - 1 ? cell : cell + " ".repeat(widths[i] - cell.length), ) .join(" ") .trimEnd(), ) .join("\n"); } export interface ReportOptions { rangeLabel: string; project?: string; daily: boolean; } export function renderReport(result: AggregateResult, opts: ReportOptions): string { const lines: string[] = []; // ── 모델별 테이블 ── const total = sumBuckets(result.byModel); const models = [...result.byModel.entries()].sort( (a, b) => b[1].totalTokens - a[1].totalTokens, ); const header = ["모델", "input", "output", "cacheRead", "total", "cost", "req"]; const body: string[][] = [ header, header.map(() => "-".repeat(10)), ...models.map(([name, b]) => [ name === TOOLS_KEY ? "(tool 내부 LLM 호출)" : name === SUMMARY_KEY ? "(compaction/요약)" : name, fmtInt(b.input), fmtInt(b.output), fmtInt(b.cacheRead), fmtInt(b.totalTokens), fmtCost(b.cost), String(b.requests), ]), [ "합계", fmtInt(total.input), fmtInt(total.output), fmtInt(total.cacheRead), fmtInt(total.totalTokens), fmtCost(total.cost), String(total.requests), ], ]; lines.push(`pi 사용량 — ${opts.rangeLabel}`); if (opts.project) lines.push(`프로젝트: ${opts.project}`); lines.push( `세션 파일 ${result.filesScanned}개 중 사용 기록 있는 세션 ${result.sessionsMatched}개 (로컬 타임존 기준)`, ); lines.push(""); lines.push(column(body)); // ── 일자별 테이블 ── if (opts.daily && result.byDay.size > 0) { const days = [...result.byDay.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)); const dHeader = ["날짜", "input", "output", "cacheRead", "total", "cost"]; const dBody: string[][] = [ dHeader, dHeader.map(() => "-".repeat(10)), ...days.map(([day, map]) => { const t = sumBuckets(map); return [ day, fmtInt(t.input), fmtInt(t.output), fmtInt(t.cacheRead), fmtInt(t.totalTokens), fmtCost(t.cost), ]; }), [ "합계", fmtInt(total.input), fmtInt(total.output), fmtInt(total.cacheRead), fmtInt(total.totalTokens), fmtCost(total.cost), ], ]; lines.push(""); lines.push("일자별"); lines.push(column(dBody)); } return lines.join("\n"); } export function renderJson(result: AggregateResult, opts: ReportOptions): string { const total = sumBuckets(result.byModel); const payload = { range: opts.rangeLabel, project: opts.project ?? null, filesScanned: result.filesScanned, sessionsMatched: result.sessionsMatched, totals: total, models: [...result.byModel.entries()] .sort((a, b) => b[1].totalTokens - a[1].totalTokens) .map(([model, b]) => ({ model, ...b })), days: opts.daily ? [...result.byDay.entries()] .sort((a, b) => (a[0] < b[0] ? -1 : 1)) .map(([date, map]) => ({ date, totals: sumBuckets(map), models: Object.fromEntries(map) })) : undefined, }; return JSON.stringify(payload, null, 2); }